Files

118 lines
8.6 KiB
Markdown

# 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`](../../scripts/extract_article_contents.py), em estrita conformidade com:
- `docs/prd_extrator_artigo_media/prd.md`
- `docs/prd_extrator_artigo_media/adr_001.md`
- `docs/prd_extrator_artigo_media/adr_002.md`
- `specs/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` (parser `html.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**:
1. **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.
2. **Identificação de Mídia Candidata Relevante**:
- **Vídeo**: tag `<video>` $\rightarrow$ `has_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>` $\rightarrow$ `has_embed = True`. Sem listas de domínios externos.
- **Múltiplas Imagens**: contagem $\ge 2$ de tags `<img>` na região editorial da matéria.
3. **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** para `extract_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.
- **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**:
```text
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ódulo `llm.py` existente é 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 via `urllib.request` em `scripts/extract_article_contents.py` é a solução mais desacoplada, limpa e com menor diff.
- **Configuração Externa dos Provedores**:
1. **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 `format` da requisição `/api/chat` com `options: {"temperature": 0.0}` e `think: false` para desabilitar explicitamente o thinking no Qwen3.5 2B.
2. **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.0` e `reasoning_effort: "low"`.
3. **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_format` com JSON Schema compatível com a instalação OpenAI-compatible utilizada, `temperature: 0.0` quando suportado, sem parâmetro inventado de reasoning.
- **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 existente `ExtractionBatchReport` onde `total_articles`, `successful_articles` e `failed_articles` refletem exclusivamente os itens persistidos nesse arquivo (artigos textuais + registros com `classification_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 `-o` e contadores.
- **Verificação Zero-Regex**:
- Reutilização do script existente [`tests/scripts/check_zero_regex.py`](../../tests/scripts/check_zero_regex.py) incluindo os arquivos de teste e módulos da feature no escopo de verificação AST.