# Feature Specification: Classificação e Roteamento de Notícias Predominantemente de Mídia **Feature Branch**: `007-media-article-routing` **Created**: 2026-08-24 **Status**: Draft **Input**: User description: "Classificação e Roteamento de Notícias Predominantemente de Mídia no script scripts/extract_article_contents.py conforme docs/prd_extrator_artigo_media (prd.md, adr_001.md, adr_002.md)" --- ## User Scenarios & Testing *(mandatory)* ### User Story 1 - Roteamento Exclusivo de Notícias Predominantemente de Mídia (Priority: P1) Como operador do pipeline de notícias, quero que publicações jornalísticas cujo conteúdo informativo principal seja uma mídia (vídeo, imagem única, coleção/múltiplas imagens, post incorporado/embed ou combinação mista de mídias) e cujo texto atue apenas como introdução, legenda, contextualização ou breve descrição dessa mídia sejam identificadas logo após o crawl e salvas em um arquivo JSON próprio (`*_media.json`), sem passar pelo pipeline multimotor textual (Trafilatura, Newspaper4k e Readability). **Why this priority**: É o objetivo central da funcionalidade: evitar processamento desnecessário de páginas não textuais pelo multimotor e separar fisicamente as publicações de mídia dos artigos textuais. **Independent Test**: Pode ser testado de forma isolada submetendo URLs cujas páginas possuam mídia predominante e texto meramente descritivo/introdutório. O sistema deve gerar o arquivo `*_media.json` com os registros correspondentes e seus metadados mínimos de rastreabilidade, sem disparar qualquer chamada aos três extratores textuais. **Acceptance Scenarios**: 1. **Cenário B (Vídeo)**: **Given** uma página carregada contendo um vídeo e texto curto apenas introdutório/contextual, **When** a detecção estrutural e o classificador processam a publicação, **Then** o classificador retorna `content_type = "media"` e `media_type = "video"`, o multimotor não é executado e o artigo é gravado em `*_media.json`. 2. **Cenário C (Imagem Única)**: **Given** uma página carregada contendo uma única imagem e texto curto descritivo, **When** o classificador processa a publicação, **Then** o classificador retorna `content_type = "media"` e `media_type = "image"`, o multimotor não é executado e o artigo é gravado em `*_media.json`. 3. **Cenário D (Múltiplas Imagens)**: **Given** uma página carregada contendo múltiplas imagens (em galeria, carousel, slideshow ou sequência vertical) e texto curto, **When** o classificador processa a publicação, **Then** o classificador retorna `content_type = "media"` e `media_type = "images"`, o multimotor não é executado e o artigo é gravado em `*_media.json`. 4. **Cenário E (Conteúdo Incorporado / Embed)**: **Given** uma página carregada contendo um post incorporado (ex: Instagram, TikTok, X) e breve contextualização textual, **When** o classificador processa a publicação, **Then** o classificador retorna `content_type = "media"` e `media_type = "embed"`, o multimotor não é executado e o artigo é gravado em `*_media.json`. 5. **Cenário F (Mídia Mista)**: **Given** uma página carregada contendo mais de uma categoria relevante de mídia (ex: vídeo e imagens) com texto meramente introdutório, **When** o classificador processa a publicação, **Then** o classificador retorna `content_type = "media"` e `media_type = "mixed"`, o multimotor não é executado e o artigo é gravado em `*_media.json`. --- ### User Story 2 - Roteamento Direto e Preservação de Notícias Textuais (Priority: P2) Como operador do pipeline textual, quero que notícias cujo conteúdo principal seja texto jornalístico substancial (mesmo que acompanhadas de fotos ilustrativas, vídeos ou infográficos) ou páginas sem qualquer elemento estrutural de mídia continuem sendo processadas normalmente pelos três motores de extração (`extract_all_engines`) e salvas no arquivo de saída textual (`*_extracted.json` ou caminho informado em `-o/--output`), preservando integralmente o formato de saída atual e a semântica de seus contadores. **Why this priority**: Garante que o pipeline textual original não sofra quebra de contrato, regressão ou perda de dados em matérias jornalísticas informativas normais. **Independent Test**: Pode ser testado submetendo: (a) páginas de texto puro sem mídia, e (b) matérias jornalísticas substanciais de múltiplos parágrafos contendo fotos ou vídeos editoriais. Ambos devem ser encaminhados ao multimotor e consolidados no arquivo de saída textual. **Acceptance Scenarios**: 1. **Cenário A (Texto sem Mídia)**: **Given** uma página carregada sem elementos estruturais candidatos a mídia na DOM, **When** a detecção estrutural prévia é executada, **Then** o classificador LLM não é chamado e o artigo é encaminhado diretamente ao multimotor textual. 2. **Cenário G (Artigo Textual Longo com Imagem)**: **Given** uma notícia com texto jornalístico substancial contendo uma ou mais imagens ilustrativas, **When** o classificador semântico avalia a publicação, **Then** retorna `content_type = "text"` com `media_type = null`, o artigo é processado pelo multimotor e salvo na saída textual. 3. **Cenário H (Artigo Textual Longo com Vídeo)**: **Given** uma notícia com texto jornalístico substancial contendo um vídeo incorporado, **When** o classificador semântico avalia a publicação, **Then** retorna `content_type = "text"` com `media_type = null`, o artigo é processado pelo multimotor e salvo na saída textual. --- ### User Story 3 - Resiliência com Fallback Operacional Sequencial e Registro de Falhas (Priority: P3) Como operador do sistema, quero que falhas puramente operacionais/técnicas no provedor primário local (Ollama) acionem sequencialmente o primeiro fallback (Groq) e, se este também falhar operacionalmente, o segundo fallback (OmniRoute), e que em caso de indisponibilidade de todos os provedores, o erro seja registrado explicitamente no JSON principal de processamento sem interromper o lote. **Why this priority**: Assegura resiliência de produção em execuções de lote sem intervenção manual, mantendo rastreabilidade estrita e isolamento de falhas. **Independent Test**: Pode ser testado simulando deterministicamente falhas operacionais e de contrato (timeout, recusa de conexão, erro HTTP, schema inválido) nos provedores intermediários e validando a transição sequencial e o comportamento de falha total, sem necessidade de chamadas a provedores reais durante a suíte normal de CI. **Acceptance Scenarios**: 1. **Cenário I (Falha no Ollama)**: **Given** o provedor primário (Qwen3.5 2B / Ollama) apresentando falha operacional (timeout, erro HTTP ou conexão recusada), **When** um artigo com mídia candidata é classificado, **Then** o sistema aciona o provedor GPT-OSS 20B / Groq e utiliza sua classificação válida para rotear o artigo. 2. **Cenário J (Falha no Ollama e Groq)**: **Given** Ollama e Groq apresentando falha operacional, **When** o artigo é classificado, **Then** o sistema aciona o provedor `cgpt-web/gpt-5.5` / OmniRoute e utiliza sua classificação válida para rotear o artigo. 3. **Cenário K (Falha Total de Todos os Provedores)**: **Given** Ollama, Groq e OmniRoute apresentando falhas operacionais consecutivas, **When** o artigo é processado, **Then** o sistema atribui `classification_status = "failed"` e mensagem de erro diagnóstica, não assume presunção arbitrária de `text` nem de `media`, não executa o multimotor, não envia o registro para `*_media.json`, registra a falha no arquivo JSON principal de processamento e continua processando os demais artigos do lote. 4. **Cenário L (Resposta fora do Schema)**: **Given** um provedor retornando resposta ilegível, truncada ou incompatível com o contrato estruturado obrigatório, **When** a validação de contrato é executada, **Then** a tentativa é tratada como falha operacional do provedor atual e o sistema avança imediatamente para o próximo provedor na cadeia de fallback. 5. **Cenário M (Suporte Multilíngue)**: **Given** páginas de notícias redigidas em qualquer um dos até 10 idiomas suportados pelo sistema (ex: espanhol, inglês, português, francês, alemão, italiano, etc.), **When** a detecção e a classificação são executadas, **Then** o sistema classifica corretamente textos curtos com mídia como `media` e textos substanciais com mídia como `text`, sem utilizar regras ou prompts específicos por idioma. --- ### Edge Cases - **Texto extremamente curto com uma única imagem (notícia curta com imagem)**: Deve ser deliberadamente classificado como `content_type = "media"` e `media_type = "image"`, pois texto com volume insuficiente não é útil para o pipeline textual. - **Avaliação de brevidade textual ("3 a 5 linhas")**: A referência de 3 a 5 linhas de texto é exclusivamente conceitual. O modelo deve avaliar semanticamente se o texto possui conteúdo jornalístico substancial por si próprio ou se funciona apenas como introdução/descrição da mídia, sem depender de contagem literal de linhas renderizadas, viewport, resolução, CSS, número de palavras ou caracteres. - **Coleções de imagens (galerias / carousels / slideshows / sequência vertical)**: O sistema não deve tentar interagir com o carousel, clicar em botões, avançar slides ou extrair URLs das imagens. Deve apenas identificar estruturalmente a presença de múltiplas imagens e classificar como `media_type = "images"`. - **Conteúdo misto sem precedência artificial**: Quando houver mais de um tipo relevante de mídia atuando como elemento informativo principal (ex: vídeo e galeria de fotos), o sistema deve classificar como `media_type = "mixed"`, sem impor regras artificiais de precedência como `video > image`. - **Falha isolada por artigo**: A falha na classificação ou no processamento de um artigo específico não pode interromper nem abortar a execução do lote. --- ## Requirements *(mandatory)* ### Functional Requirements #### Detecção Estrutural Prévia - **FR-001**: O sistema MUST analisar estruturalmente a DOM carregada pelo crawler antes de qualquer chamada aos motores Trafilatura, Newspaper4k e Readability. - **FR-002**: A análise estrutural da DOM MUST ser realizada exclusivamente via parser HTML/DOM (navegação por nós, tags, atributos e contagem de elementos), sendo estritamente proibido o uso de regex (`re`), listas de palavras-chave ou heurísticas semânticas por idioma em toda a nova implementação. - **FR-003**: Quando nenhuma mídia candidata estiver presente como elemento estruturalmente relevante ao conteúdo da publicação na DOM carregada (sem imagem, vídeo, iframe/embed, object ou estruturas DOM equivalentes associadas à publicação), o artigo MUST seguir diretamente para o pipeline multimotor textual sem chamada ao classificador LLM. - **FR-004**: Quando houver mídia candidata presente e estruturalmente relevante ao conteúdo da publicação na DOM carregada (como imagem, vídeo, iframe/embed, object ou estruturas equivalentes associadas à publicação), o sistema MUST submeter uma representação compacta da publicação ao classificador de conteúdo. A mera presença de mídia em outras regiões da página não associadas ao conteúdo da publicação não deve, isoladamente, tornar o artigo candidato. #### Entrada e Escopo do Classificador - **FR-005**: O classificador MUST receber apenas dados textuais e estruturais compactos já disponíveis na página carregada (título, blocos textuais relevantes, informação estrutural de mídias presentes e quantidade/tipos encontrados), evitando o envio do HTML completo quando a representação compacta contiver a mesma informação. - **FR-006**: O sistema MUST NOT baixar imagens, baixar vídeos, executar OCR, executar visão computacional, assistir vídeos, transcrever áudios, navegar em carousels/slideshows nem chamar APIs externas de plataformas de mídia. #### Contrato Estruturado do Classificador - **FR-007**: A saída emitida pelo classificador LLM MUST conter exclusivamente os campos `content_type` e `media_type` em formato JSON estruturado (sem campos como `provider_used`, `status`, `error_message`, `confidence`, `rationale`, `summary`, `keywords`, `evidence` ou `tradução`). - **FR-008**: O campo `content_type` emitido pelo modelo MUST aceitar exclusivamente os valores `"text"` ou `"media"`. - **FR-009**: O campo `media_type` emitido pelo modelo MUST aceitar exclusivamente os valores `"video"`, `"image"`, `"images"`, `"embed"`, `"mixed"` quando `content_type = "media"`, e MUST ser obrigatoriamente `null` quando `content_type = "text"`. - **FR-010**: O prompt enviado ao classificador MUST ser único, conciso, comum a todos os idiomas e solicitar exclusivamente a classificação necessária (`content_type` e `media_type`). O prompt MUST NOT solicitar reasoning, confidence, rationale, summary, keywords, evidence ou tradução. Reasoning deliberativo adicional não deve ser habilitado quando não for necessário para produzir o contrato estruturado da classificação, e novos campos não devem ser adicionados à resposta. - **FR-011**: Sempre que suportado pelo provedor, a requisição ao modelo MUST utilizar structured output nativo (JSON Schema / response_format). Toda resposta MUST ser estritamente validada contra o contrato antes de ser aceita. #### Roteamento, Persistência e Regras de Arquivos - **FR-012**: Artigos classificados como `content_type = "text"` MUST seguir normalmente pelo fluxo textual existente, executando os três motores de extração (`extract_all_engines`) e sendo salvos no arquivo JSON de saída textual. - **FR-013**: Artigos classificados como `content_type = "media"` MUST NOT executar os motores Trafilatura, Newspaper4k ou Readability. - **FR-014**: Artigos classificados como `content_type = "media"` MUST ser gravados em arquivo JSON de mídia dedicado com sufixo `_media.json`, obedecendo às seguintes regras de nomenclatura e diretório: - **Sem `-o/--output`**: para uma entrada `