Files
TextNLPClassifierApp/specs/007-media-article-routing/plan.md
T

16 KiB

Implementation Plan: Classificação e Roteamento de Notícias Predominantemente de Mídia

Branch: 007-media-article-routing | Date: 2026-08-24 | Spec: spec.md

Input: Feature specification from /specs/007-media-article-routing/spec.md


1. Summary

Esta feature adiciona ao script scripts/extract_article_contents.py a capacidade de identificar publicações cujo conteúdo informativo principal seja mídia (vídeo, imagem única, múltiplas imagens/galeria, embed ou mídia mista) e cujo texto atue apenas como introdução ou contextualização.

Após o crawl com Foxcape, a página sofre uma análise estrutural na DOM (zero-regex via BeautifulSoup). Se nenhuma mídia relevante for detectada, o artigo segue diretamente para o multimotor textual (extract_all_engines). Se houver mídia relevante, o texto editorial normalizado e o resumo estrutural são avaliados por uma cadeia sequencial de LLMs com fallback puramente operacional (Ollama/Qwen3.5 2B \rightarrow Groq/openai/gpt-oss-20b \rightarrow OmniRoute/cgpt-web/gpt-5.5). Artigos classificados como media são desviados antes dos três motores textuais e gravados no arquivo de mídia dedicado (*_media.json), enquanto artigos textuais e eventuais falhas de classificação continuam para o arquivo principal (*_extracted.json).


2. Technical Context

  • Linguagem & Tipagem: Python >=3.10 com anotações de tipo completas em conformidade com o princípio de qualidade do repositório (Constitution §Technical Constraints).
  • Dependências Reutilizadas: beautifulsoup4, trafilatura, newspaper4k, readability-lxml, foxcape (todas já instaladas e ativas no projeto).
  • Cliente HTTP para LLMs: Biblioteca padrão do Python (urllib.request, urllib.error, json), sem novas dependências externas.
  • Armazenamento / I/O: Arquivos JSON no filesystem local (*_extracted.json e *_media.json).
  • Suíte de Testes: pytest com simulação determinística dos 5 estados da cadeia de provedores; script estático existente tests/scripts/check_zero_regex.py.
  • Tipo de Projeto: Pipeline de linha de comando (CLI) existente.

3. Constitution Check

Princípio Constitucional Avaliação Técnica & Rastreabilidade Status
I. Modularity & CLI-First Integrado diretamente em scripts/extract_article_contents.py, preservando 100% dos parâmetros CLI (-i, -o, -l, --lang, -t, -s) e exit codes (0, 1, 2, 130). PASS
II. Determinism & Data Integrity Contrato estruturado de 2 campos no LLM com configuração determinística; parsing estrito de DOM; preservação intacta dos metadados de entrada (input_meta). PASS
III. Multi-Engine & Fault-Tolerant Fallback Artigos textuais continuam processados pelos 3 motores; cadeia sequencial com 2 níveis de contingência operacional (Ollama \rightarrow Groq \rightarrow OmniRoute); falha isolada por artigo. PASS
IV. Test-First & Empirical Validation Suíte de testes cobrindo cenários A a M e os 5 estados da cadeia de provedores na CI sem chamadas de rede externas; verificação estática Zero-Regex. PASS
V. Observability & Structured Logging Contabilização e emissão das 11 métricas operacionais obrigatórias no resumo/log stderr existente; logs de fallbacks e provedor sem expor segredos ou payload integral. PASS

4. Arquitetura e Decisões Técnicas Fechadas

4.1 Gate Estrutural na DOM (Zero-Regex)

  1. Localização da Região Editorial:
    • Inspecionar a DOM carregada buscando nós na seguinte ordem de precedência: <article>, <main>, <div role="main">, ou <body> caso nenhuma anterior exista.
    • Descartar nós estruturais fora do conteúdo da matéria (<header>, <nav>, <footer>, <aside>).
    • Sem regex, sem heurísticas de classes CSS, sem seletores específicos de sites e sem detector de anúncios.
  2. Critério de Mídia Candidata Relevante:
    • Vídeo: tags <video> \rightarrow has_video = True. Tags <source> pertencentes a <video> não são contadas isoladamente.
    • Imagem: contagem das tags <img> reais na região editorial. Wrappers como <figure> e <picture> não incrementam ou duplicam o contador.
    • Embed: tags <iframe>, <embed>, <object> \rightarrow has_embed = True. Sem listas de domínios externos.
    • Múltiplas Imagens: contagem \ge 2 de tags <img> na região editorial.
  3. Decisão do Gate:
    • Retorna MediaCandidateInfo(has_candidate_media, has_video, image_count, has_embed).
    • Se has_candidate_media == False \rightarrow desvio direto para extract_all_engines() (zero chamadas LLM).
    • Se has_candidate_media == True \rightarrow montagem de payload e execução da cadeia LLM.

4.2 Payload Compacto do Classificador

  • Campos Extraídos:
    • title: Título obtido da tag <title> ou <h1> da matéria.
    • text_content: Textos dos parágrafos (<p>) da região editorial, normalizados sem tags HTML e preservando o conteúdo jornalístico substancial da matéria (sem truncamento arbitrário de caracteres).
    • media_summary: Resumo dos elementos de mídia identificados no gate (has_video, image_count, has_embed).
  • Prompt Único Multilíngue: Instrução concisa solicitando estritamente a classificação em content_type (text ou media) e media_type (video, image, images, embed, mixed ou null), sem reasoning deliberativo, sem tradução e sem resumos.

4.3 Cadeia Sequencial de Provedores, Structured Output e Controle de Reasoning

  1. Configuração dos Provedores e Reasoning:
    • Ollama (Primário):
      • Endpoint: os.environ.get("OLLAMA_ENDPOINT", "http://localhost:11434")
      • Modelo: os.environ.get("OLLAMA_MODEL", "qwen3.5:2b")
      • Timeout: int(os.environ.get("OLLAMA_TIMEOUT", "10"))
      • Structured Output & Reasoning: passa o JSON Schema diretamente no campo format da requisição /api/chat, com options: {"temperature": 0.0} e think: false para desabilitar explicitamente thinking no Qwen3.5 2B.
    • Groq (1º Fallback):
      • Endpoint: os.environ.get("GROQ_ENDPOINT", "https://api.groq.com/openai/v1/chat/completions")
      • API Key: os.environ.get("GROQ_API_KEY")
      • Modelo: os.environ.get("GROQ_MODEL", "openai/gpt-oss-20b")
      • Timeout: int(os.environ.get("GROQ_TIMEOUT", "15"))
      • Structured Output & Reasoning: response_format={"type": "json_schema", "json_schema": {"name": "media_classifier", "strict": True, "schema": <schema>}}, temperature: 0.0 e reasoning_effort: "low".
    • OmniRoute (2º Fallback):
      • Endpoint: os.environ.get("OMNIROUTE_ENDPOINT") (configuração externa obrigatória sem default inventado)
      • API Key: os.environ.get("OMNIROUTE_API_KEY")
      • Modelo: os.environ.get("OMNIROUTE_MODEL", "cgpt-web/gpt-5.5")
      • Timeout: int(os.environ.get("OMNIROUTE_TIMEOUT", "20"))
      • Structured Output & Reasoning: response_format com JSON Schema compatível com a instalação OpenAI-compatible utilizada, temperature: 0.0 quando suportado, sem parâmetro inventado de reasoning.
  2. Regra de Transição e Validação de Schema:
    • A aplicação sempre valida os dois campos recebidos:
      • content_type \in {"text", "media"};
      • se content_type == "text", media_type deve ser None;
      • se content_type == "media", media_type deve ser um de {"video", "image", "images", "embed", "mixed"}.
    • Resposta válida \rightarrow encerra a cadeia imediatamente (primeira resposta válida conclui).
    • Falha operacional (timeout, conexão recusada, erro HTTP ou schema incompatível) \rightarrow avança imediatamente para o próximo provedor na ordem estrita Ollama \rightarrow Groq \rightarrow OmniRoute.
    • Sem retries por provedor, sem exponential backoff, sem circuit breakers e sem discovery dinâmico.
    • Se todos os 3 provedores falharem \rightarrow artigo recebe classification_status = "failed" e error_message, sendo gravado inline no JSON principal sem multimotor e sem interromper o lote.

4.4 I/O, Nomenclatura de Arquivos e Contadores

  • Regras de Resolução de Caminhos:
    • Sem -o: entrada out/river_plate.json \rightarrow textual out/river_plate_extracted.json, mídia out/river_plate_media.json.
    • Com -o out/dir/saida.json \rightarrow textual out/dir/saida.json, mídia out/dir/saida_media.json.
    • Nenhuma nova flag CLI (sem --media-output).
  • Arquivo de Mídia (*_media.json):
    • O arquivo *_media.json MUST ser gerado em toda execução bem-sucedida do lote.
    • Envelope contendo estritamente { "articles": [ MediaArticle... ] }.
    • Quando nenhum artigo for classificado como mídia no lote, o arquivo conterá exatamente {"articles": []} (sem omissão condicional).
  • Arquivo Textual Principal (*_extracted.json): Envelope ExtractionBatchReport onde os contadores refletem estritamente os registros presentes no arquivo:
    • total_articles: contagem de registros presentes no array articles;
    • successful_articles: artigos textuais processados com sucesso pelo multimotor;
    • failed_articles: registros de falha presentes (falhas de crawl ou com classification_status = "failed").
  • Preservação de Metadados (input_meta): O dicionário de metadados da entrada é preservado integralmente em input_meta para todos os registros (textuais, mídia e falhas), exigindo titulo e url, sem inventar valores default e sem descartar campos adicionais.

4.5 Observabilidade, Logging e Pontos Exatos de Tracking das 11 Métricas

As 11 métricas são contabilizadas em memória (dicionário local dentro de process_batch) e registradas nos seguintes pontos exatos:

  1. total_evaluated: incrementado quando um artigo com crawl bem-sucedido entra no gate estrutural da DOM;
  2. text: incrementado quando: (a) não existe mídia candidata no gate (bypass direto), OU (b) LLM retorna content_type="text";
  3. media: incrementado exclusivamente quando o LLM retorna content_type="media";
  4. media/video: incrementado em conjunto com media quando media_type == "video";
  5. media/image: incrementado em conjunto com media quando media_type == "image";
  6. media/images: incrementado em conjunto com media quando media_type == "images";
  7. media/embed: incrementado em conjunto com media quando media_type == "embed";
  8. media/mixed: incrementado em conjunto com media quando media_type == "mixed";
  9. fallback_groq: incrementado imediatamente antes de despachar a chamada HTTP para o Groq;
  10. fallback_omniroute: incrementado imediatamente antes de despachar a chamada HTTP para o OmniRoute;
  11. classification_failed: incrementado uma única vez por artigo quando os 3 provedores falharem cumulativamente.
  • Logging no stderr: Registro de provider utilizado, fallbacks acionados, classificação e falhas. Respeita a flag -s/--silent. Segredos e payloads textuais integrais nunca são logados por padrão.

5. Organização de Arquivos (Mínima e Sem Classes Desnecessárias)

Funções Implementadas em scripts/extract_article_contents.py

Para manter o mínimo de código e evitar classes desnecessárias:

  • detect_candidate_media(soup: BeautifulSoup) -> MediaCandidateInfo: Função pura de inspeção estrutural na DOM (sem regex).
  • build_compact_payload(soup: BeautifulSoup, candidate_info: MediaCandidateInfo) -> str: Função pura de extração e normalização do texto editorial e resumo estrutural.
  • classify_media_content(payload: str) -> tuple[MediaClassification | None, str | None]: Função que orquestra a chamada sequencial aos 3 provedores via urllib.request e validação estrita.
  • save_media_json(articles: list[dict[str, Any]], output_path: Path) -> None: Gravação do arquivo de mídia com a mesma estratégia de escrita JSON do script existente.
  • Atualização do loop process_batch para incorporar o desvio, gravação de *_media.json e contabilidade das 11 métricas.

(Nota de Reutilização: O módulo src/tools/adapters/llm.py foi inspecionado; ele é altamente especializado na desambiguação de entidades ECP da feature 001 com classes acopladas, de modo que a integração via funções diretas com urllib.request em extract_article_contents.py é a solução mais desacoplada, limpa e com menor diff).

Arquivo Existente Reutilizado

Novos Arquivos de Teste

  • tests/unit/test_media_classifier.py:
    • Testes do gate DOM (vídeo sem falso positivo de source, contagem correta de imagens sem duplicar figure/picture, embeds);
    • Testes de montagem do payload com texto completo;
    • Testes de validação de schema e simulação dos 5 estados da cadeia de provedores.
  • tests/integration/test_media_routing.py:
    • Testes de integração em lote cobrindo cenários A a M;
    • Validação de caminhos -o, contadores, geração incondicional de *_media.json (com [] quando vazio) e persistência de falha inline.

6. Matriz de Rastreabilidade (Requisitos \rightarrow Código \rightarrow Testes)

Requisito Descrição Implementação em extract_article_contents.py Teste Correspondente
FR-001 - FR-004 Análise estrutural da DOM e desvio sem LLM detect_candidate_media() test_media_classifier.py::test_structural_gate_*
FR-005 - FR-006 Payload compacto e proibições de mídia binária build_compact_payload() test_media_classifier.py::test_compact_payload_*
FR-007 - FR-011 Contrato estruturado de 2 campos e Structured Output classify_media_content() test_media_classifier.py::test_schema_validation_*
FR-012 - FR-017 Roteamento textual/mídia, *_media.json e contadores process_batch(), save_media_json() test_media_routing.py::test_routing_and_counters_*
FR-018 - FR-024 Cadeia sequencial Ollama \rightarrow Groq \rightarrow OmniRoute e falha total classify_media_content() test_media_classifier.py::test_provider_chain_*
FR-025 - FR-026 Configuração externa e mascaramento de segredos classify_media_content() test_media_routing.py::test_security_secrets_masked
FR-027 - FR-028 11 métricas operacionais e logging process_batch() test_media_routing.py::test_metrics_logging
FR-029 - FR-030 Suporte multilíngue e compatibilidade CLI parse_arguments(), process_batch() test_media_routing.py::test_cli_compatibility
SC-007 Zero-Regex em toda a nova implementação AST Checker tests/scripts/check_zero_regex.py

7. Invariantes e Limites de Escopo

Fica expressamente estabelecido que a implementação NÃO DEVE introduzir:

  • Bancos de dados, filas de mensagens, DLQ ou novos workers;
  • Novos serviços, microserviços ou processos autônomos;
  • Pipelines adicionais de NLP ou frameworks de agentes (LangChain, LangGraph, LLM-as-a-judge);
  • Votação, consenso entre modelos ou fallbacks por incerteza subjetiva;
  • Download de mídia binária, OCR, visão computacional ou transcrição de áudio/vídeo;
  • Interação com carousels ou chamadas a APIs de redes sociais;
  • Alterações no comportamento de carregamento do Foxcape ou na lógica interna de Trafilatura, Newspaper4k e Readability;
  • Criação de novas flags CLI como --media-output, novos arquivos como *_failed.json ou classes desnecessárias como MediaBatchReport e MediaMetricsCollector.