8.6 KiB
8.6 KiB
Phase 0 Research: Classificação e Roteamento de Notícias Predominantemente de Mídia
Feature: 007-media-article-routing
Date: 2026-08-24
Status: Completed
1. Contexto & Objetivos da Pesquisa
Esta pesquisa detalha as decisões técnicas para a implementação da classificação semântica e roteamento de artigos predominantemente de mídia em scripts/extract_article_contents.py, em estrita conformidade com:
docs/prd_extrator_artigo_media/prd.mddocs/prd_extrator_artigo_media/adr_001.mddocs/prd_extrator_artigo_media/adr_002.mdspecs/007-media-article-routing/spec.md.specify/memory/constitution.md
2. Decisões Técnicas Consolidadas
Decisão 1: Algoritmo de Detecção Estrutural de Mídia Candidata na DOM (Zero-Regex)
- Decisão: Utilizar
BeautifulSoup(parserhtml.parser, biblioteca já instalada e utilizada no repositório) para inspecionar a região da DOM associada ao conteúdo da publicação antes de qualquer chamada LLM ou execução do multimotor textual. - Algoritmo Estrutural de Localização:
- Delimitação da Região da Publicação:
- Localizar nós semânticos de conteúdo editorial na seguinte ordem de precedência: tag
<article>, tag<main>, tag<div role="main">, ou tag<body>caso nenhuma anterior exista. - Isolar a análise dessa região, descartando elementos 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.
- Localizar nós semânticos de conteúdo editorial na seguinte ordem de precedência: tag
- Identificação de Mídia Candidata Relevante:
- Vídeo: tag
<video>\rightarrowhas_video = True. Tags<source>pertencentes a<video>não são contadas isoladamente. - Imagem: contagem de tags
<img>reais na região editorial. Wrappers como<figure>e<picture>não incrementam nem duplicam a contagem. - 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 da matéria.
- Vídeo: tag
- Resultado Estrutural:
- Retorna objeto tipado
MediaCandidateInfo(has_candidate_media: bool, has_video: bool, image_count: int, has_embed: bool). - Se
has_candidate_media == False: o artigo segue diretamente paraextract_all_engines()sem qualquer chamada a LLM. - Se
has_candidate_media == True: constrói o payload compacto e invoca a cadeia sequencial de classificação LLM.
- Retorna objeto tipado
- Delimitação da Região da Publicação:
- Rationale: Filtro prévio determinístico e de custo zero de LLM, sem regex, sem dicionários por idioma e sem seletores amarrados a domínios específicos.
Decisão 2: Especificação do Payload Compacto Enviado ao Classificador LLM
- Decisão: Extrair da DOM carregada uma estrutura lógica compacta com os dados essenciais para a decisão semântica:
title: Título da matéria extraído da tag<title>ou<h1>da região editorial.text_content: Textos dos parágrafos (<p>) da região editorial normalizados e limpos de marcação HTML, preservando todo o conteúdo jornalístico substancial da matéria sem truncamentos arbitrários de caracteres.media_summary: Indicadores estruturais identificados no gate (has_video: bool,image_count: int,has_embed: bool).
- Formato do Prompt Único Multilíngue:
You are an editorial news classifier. Classify if this news publication is predominantly media or substantive journalistic text. Publication Title: {title} Structural Media Present: Video={has_video}, ImagesCount={image_count}, Embed={has_embed} Text Content: {text_content} Definitions: - "media": The primary informative content is in the media (video, single image, multiple images/gallery, social embed, or mixed), and the text functions essentially as a brief introduction, caption, contextualization, or description. - "text": The publication contains substantive journalistic text on its own, even if accompanied by illustrative media. Respond ONLY with a JSON object matching this exact schema: {"content_type": "text" | "media", "media_type": "video" | "image" | "images" | "embed" | "mixed" | null} Rules: - If content_type is "text", media_type MUST be null. - If content_type is "media", media_type MUST be one of: "video", "image", "images", "embed", "mixed". - Rationale: Payload enxuto sem HTML desnecessário, permitindo decisão semântica precisa pelo modelo.
Decisão 3: Cliente HTTP e Cadeia Sequencial de Provedores LLM
- Decisão de Cliente HTTP: Utilizar funções diretas com a biblioteca padrão do Python (
urllib.request/urllib.error/json), que já é o padrão estabelecido no repositório, sem adicionar novas dependências ao projeto. - Reutilização de
src/tools/adapters/llm.py: O módulollm.pyexistente é altamente especializado na desambiguação de entidades ECP da feature 001 com classes acopladas (ECPSnapshot,DecisionCategory); acoplá-lo à classificação de mídia criaria emaranhamento desnecessário de domínios. A implementação com funções diretas viaurllib.requestemscripts/extract_article_contents.pyé a solução mais desacoplada, limpa e com menor diff. - Configuração Externa dos Provedores:
- 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: passa o JSON Schema diretamente no campo
formatda requisição/api/chatcomoptions: {"temperature": 0.0}ethink: falsepara desabilitar explicitamente o 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:
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 no ambiente 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:
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):
- Regras de Execução e Fallback:
- Execução estritamente sequencial. Toda resposta que validar contra o schema de 2 campos encerra a cadeia com sucesso.
- Falha operacional (timeout, recusa de conexão, erro HTTP ou schema inválido) transita imediatamente para o próximo provedor.
- Se todos falharem: atribui
classification_status = "failed"e mensagem diagnóstica, registrando o artigo inline no JSON principal sem multimotor e sem interromper o lote.
Decisão 4: Estrutura de Arquivos e Semântica de Contadores
- Saída de Mídia (
*_media.json): Envelope físico contendo estritamente{ "articles": [ MediaArticle... ] }. - Saída Textual (
*_extracted.json): Envelope existenteExtractionBatchReportondetotal_articles,successful_articlesefailed_articlesrefletem exclusivamente os itens persistidos nesse arquivo (artigos textuais + registros comclassification_status = "failed"). - Métricas de Execução: As 11 métricas obrigatórias são contabilizadas em memória e registradas no log do stderr ao final do lote (respeitando a flag
--silent).
Decisão 5: Estratégia de Testes e Zero-Regex
- Testes Determinísticos (CI):
- Testes unitários (
tests/unit/test_media_classifier.py) cobrindo gate estrutural DOM, montagem de compact payload e simulação dos 5 estados da cadeia de provedores sem chamadas de rede externas. - Testes de integração (
tests/integration/test_media_routing.py) cobrindo o fluxo em lote completo, cenários A a M, roteamento com e sem-oe contadores.
- Testes unitários (
- Verificação Zero-Regex:
- Reutilização do script existente
tests/scripts/check_zero_regex.pyincluindo os arquivos de teste e módulos da feature no escopo de verificação AST.
- Reutilização do script existente