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.10com 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.jsone*_media.json). - Suíte de Testes:
pytestcom simulação determinística dos 5 estados da cadeia de provedores; script estático existentetests/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)
- 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.
- Inspecionar a DOM carregada buscando nós na seguinte ordem de precedência:
- Critério de Mídia Candidata Relevante:
- Vídeo: tags
<video>\rightarrowhas_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>\rightarrowhas_embed = True. Sem listas de domínios externos. - Múltiplas Imagens: contagem
\ge 2de tags<img>na região editorial.
- Vídeo: tags
- Decisão do Gate:
- Retorna
MediaCandidateInfo(has_candidate_media, has_video, image_count, has_embed). - Se
has_candidate_media == False\rightarrowdesvio direto paraextract_all_engines()(zero chamadas LLM). - Se
has_candidate_media == True\rightarrowmontagem de payload e execução da cadeia LLM.
- Retorna
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(textoumedia) emedia_type(video,image,images,embed,mixedounull), sem reasoning deliberativo, sem tradução e sem resumos.
4.3 Cadeia Sequencial de Provedores, Structured Output e Controle de Reasoning
- 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
formatda requisição/api/chat, comoptions: {"temperature": 0.0}ethink: falsepara desabilitar explicitamente thinking no Qwen3.5 2B.
- Endpoint:
- 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.0ereasoning_effort: "low".
- Endpoint:
- 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_formatcom JSON Schema compatível com a instalação OpenAI-compatible utilizada,temperature: 0.0quando suportado, sem parâmetro inventado de reasoning.
- Endpoint:
- Ollama (Primário):
- 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_typedeve serNone; - se
content_type == "media",media_typedeve ser um de{"video", "image", "images", "embed", "mixed"}.
- Resposta válida
\rightarrowencerra a cadeia imediatamente (primeira resposta válida conclui). - Falha operacional (timeout, conexão recusada, erro HTTP ou schema incompatível)
\rightarrowavança imediatamente para o próximo provedor na ordem estrita Ollama\rightarrowGroq\rightarrowOmniRoute. - Sem retries por provedor, sem exponential backoff, sem circuit breakers e sem discovery dinâmico.
- Se todos os 3 provedores falharem
\rightarrowartigo recebeclassification_status = "failed"eerror_message, sendo gravado inline no JSON principal sem multimotor e sem interromper o lote.
- A aplicação sempre valida os dois campos recebidos:
4.4 I/O, Nomenclatura de Arquivos e Contadores
- Regras de Resolução de Caminhos:
- Sem
-o: entradaout/river_plate.json\rightarrowtextualout/river_plate_extracted.json, mídiaout/river_plate_media.json. - Com
-o out/dir/saida.json\rightarrowtextualout/dir/saida.json, mídiaout/dir/saida_media.json. - Nenhuma nova flag CLI (sem
--media-output).
- Sem
- Arquivo de Mídia (
*_media.json):- O arquivo
*_media.jsonMUST 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).
- O arquivo
- Arquivo Textual Principal (
*_extracted.json): EnvelopeExtractionBatchReportonde os contadores refletem estritamente os registros presentes no arquivo:total_articles: contagem de registros presentes no arrayarticles;successful_articles: artigos textuais processados com sucesso pelo multimotor;failed_articles: registros de falha presentes (falhas de crawl ou comclassification_status = "failed").
- Preservação de Metadados (
input_meta): O dicionário de metadados da entrada é preservado integralmente eminput_metapara todos os registros (textuais, mídia e falhas), exigindotituloeurl, 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:
total_evaluated: incrementado quando um artigo com crawl bem-sucedido entra no gate estrutural da DOM;text: incrementado quando: (a) não existe mídia candidata no gate (bypass direto), OU (b) LLM retornacontent_type="text";media: incrementado exclusivamente quando o LLM retornacontent_type="media";media/video: incrementado em conjunto commediaquandomedia_type == "video";media/image: incrementado em conjunto commediaquandomedia_type == "image";media/images: incrementado em conjunto commediaquandomedia_type == "images";media/embed: incrementado em conjunto commediaquandomedia_type == "embed";media/mixed: incrementado em conjunto commediaquandomedia_type == "mixed";fallback_groq: incrementado imediatamente antes de despachar a chamada HTTP para o Groq;fallback_omniroute: incrementado imediatamente antes de despachar a chamada HTTP para o OmniRoute;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 viaurllib.requeste 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_batchpara incorporar o desvio, gravação de*_media.jsone 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
tests/scripts/check_zero_regex.py:- Inclusão dos novos arquivos de teste no escopo de validação estática.
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.jsonou classes desnecessárias comoMediaBatchReporteMediaMetricsCollector.