# PRD — Classificação e Roteamento de Notícias Predominantemente de Mídia ## 1. Visão geral O sistema atual processa URLs de notícias, obtém a página completamente carregada pelo crawler e encaminha o HTML para múltiplos motores de extração textual. Esse comportamento funciona para notícias cujo conteúdo principal é textual. Entretanto, alguns veículos publicam conteúdos jornalísticos cujo conteúdo principal é uma mídia, por exemplo: * vídeo; * uma única imagem; * múltiplas imagens; * conteúdo incorporado; * combinação de mais de um tipo de mídia. Nesses casos, o texto da publicação é curto e funciona essencialmente como uma introdução, descrição ou contextualização da mídia. Essas publicações não devem passar pelo pipeline multimotor textual. O sistema deve identificá-las imediatamente após o crawl e roteá-las para um arquivo JSON separado. --- # 2. Objetivo Adicionar ao pipeline atual uma etapa pequena de classificação capaz de distinguir: ```text text ``` de: ```text media ``` Quando o conteúdo for classificado como `media`, também deve ser identificado como: ```text video image images embed mixed ``` O objetivo desta feature é exclusivamente: 1. identificar publicações predominantemente de mídia; 2. evitar processamento desnecessário pelo multimotor textual; 3. separar fisicamente essas publicações em um JSON próprio. A feature não deve extrair, interpretar, baixar ou processar a mídia. --- # 3. Motivação O multimotor atual foi criado para extração de conteúdo textual. Executar Trafilatura, Newspaper4k e Readability em uma página cujo conteúdo real é um vídeo, uma imagem ou uma coleção de imagens: * não agrega valor; * consome processamento desnecessariamente; * pode produzir conteúdo textual irrelevante ou insuficiente; * mistura publicações de natureza diferente no mesmo pipeline. A separação deve acontecer antes do multimotor. --- # 4. Princípios obrigatórios A implementação deve seguir estes princípios: 1. Production-ready. 2. Mínimo de código necessário para atender integralmente aos requisitos. 3. Sem overengineering. 4. Sem alteração desnecessária do crawler atual. 5. Sem alteração desnecessária dos motores existentes. 6. Sem novos serviços. 7. Sem filas. 8. Sem agentes. 9. Sem LangGraph. 10. Sem votação entre modelos. 11. Sem pipelines adicionais de NLP. 12. Sem análise visual da mídia. 13. Sem download de mídia. 14. Sem regex. 15. Independente de idioma. 16. Compatível com os até 10 idiomas processados pelo sistema. 17. Fallback de LLM exclusivamente por falha operacional. --- # 5. Escopo funcional ## 5.1 Fluxo principal O novo fluxo deve ser: ```text URL ↓ Crawler atual ↓ Página completamente carregada ↓ Detecção estrutural de presença de mídia ↓ ├── nenhuma mídia candidata │ ↓ │ pipeline textual atual │ └── existe mídia candidata ↓ classificador de conteúdo ↓ ├── text │ ↓ │ pipeline textual atual │ └── media ↓ *_media.json ``` O classificador deve executar antes de qualquer chamada a: * Trafilatura; * Newspaper4k; * Readability. --- # 6. Definição de conteúdo predominantemente de mídia Uma publicação deve ser classificada como `media` quando: > O conteúdo informativo principal da publicação está em uma mídia e o texto existente funciona essencialmente como introdução, legenda, contextualização ou breve descrição dessa mídia. Exemplos: ```text Título "Mira el increíble gol marcado durante el partido." [VÍDEO] ``` Resultado: ```json { "content_type": "media", "media_type": "video" } ``` --- Exemplo: ```text Título "Estas fueron las mejores imágenes de la celebración." [IMAGEM] [IMAGEM] [IMAGEM] [IMAGEM] ``` Resultado: ```json { "content_type": "media", "media_type": "images" } ``` --- Exemplo: ```text Título "El jugador publicó el siguiente mensaje." [INSTAGRAM EMBED] ``` Resultado: ```json { "content_type": "media", "media_type": "embed" } ``` --- # 7. Conteúdo textual normal A presença de mídia não transforma automaticamente uma notícia em `media`. Exemplo: ```text Título Parágrafo 1 Parágrafo 2 Parágrafo 3 Parágrafo 4 Parágrafo 5 Parágrafo 6 [VÍDEO] Parágrafo 7 Parágrafo 8 ``` Resultado: ```json { "content_type": "text" } ``` Essa publicação deve continuar normalmente pelo multimotor. --- # 8. Texto curto A referência de negócio é um texto equivalente aproximadamente a três a cinco linhas de conteúdo editorial. Essa referência NÃO deve ser implementada utilizando quantidade literal de linhas renderizadas. Quantidade de linhas depende de: * viewport; * resolução; * fonte; * CSS; * dispositivo; * responsividade. O modelo deve avaliar semanticamente se o texto possui conteúdo jornalístico substancial por si próprio ou se apenas introduz/descreve a mídia. Não deve existir heurística linguística baseada em idioma. --- # 9. Tipos de mídia ## 9.1 `video` Conteúdo cujo elemento informativo principal é vídeo. ```json { "content_type": "media", "media_type": "video" } ``` --- ## 9.2 `image` Conteúdo cujo elemento informativo principal é uma única imagem. ```json { "content_type": "media", "media_type": "image" } ``` Não é necessário identificar se a imagem representa: * fotografia; * infográfico; * gráfico; * ilustração; * charge; * qualquer outra categoria visual. --- ## 9.3 `images` Conteúdo cujo elemento informativo principal é composto por múltiplas imagens. Pode ser: * carousel; * galeria; * slideshow; * imagens apresentadas sequencialmente; * imagens simplesmente exibidas uma abaixo da outra. O sistema não precisa identificar qual mecanismo visual o site utiliza. ```json { "content_type": "media", "media_type": "images" } ``` --- ## 9.4 `embed` Conteúdo incorporado cuja informação principal esteja em um elemento externo incorporado à página. Exemplos possíveis: * Instagram; * TikTok; * X; * outros embeds. ```json { "content_type": "media", "media_type": "embed" } ``` --- ## 9.5 `mixed` Conteúdo predominantemente de mídia que possui mais de um tipo relevante de mídia. Exemplo: ```text texto curto [VÍDEO] [IMAGEM] [IMAGEM] ``` Resultado: ```json { "content_type": "media", "media_type": "mixed" } ``` --- # 10. Notícia curta com uma imagem Uma publicação com texto muito curto e uma única imagem pode ser classificada como: ```json { "content_type": "media", "media_type": "image" } ``` Mesmo que tecnicamente pudesse ser considerada uma notícia textual curta. Esse comportamento é intencional. Conteúdo textual insuficiente não é útil para o pipeline textual atual. --- # 11. Detecção estrutural prévia Antes de chamar qualquer LLM, o sistema deve verificar estruturalmente se existe mídia candidata na DOM carregada. Essa análise: * deve utilizar parser de DOM; * não pode utilizar regex; * não deve interpretar semanticamente o conteúdo; * não deve identificar palavras-chave; * não deve depender do idioma. Elementos estruturais relevantes podem incluir elementos HTML representando: * imagens; * vídeo; * múltiplas imagens; * iframe; * embed; * object; * estruturas equivalentes já presentes na DOM carregada. O objetivo dessa etapa é apenas evitar uma chamada ao classificador quando não existe mídia candidata. Se não existir mídia candidata: ```text → text → pipeline atual ``` Se existir mídia candidata: ```text → executar classificação LLM ``` --- # 12. Entrada do classificador O classificador não deve receber mídia binária. Não deve: * baixar imagens; * baixar vídeos; * executar OCR; * executar visão computacional; * assistir vídeo; * transcrever áudio; * navegar dentro do carousel; * chamar APIs dos providers da mídia. O classificador deve utilizar somente informações já disponíveis na página carregada. A entrada deve ser compacta e conter apenas os dados necessários para decidir: ```text text ``` ou: ```text media ``` e, no segundo caso, o `media_type`. A entrada pode conter: * título; * blocos textuais relevantes presentes na DOM; * informação estrutural sobre mídia presente; * quantidade/tipos estruturais de mídia encontrados. Não deve ser enviado o HTML completo caso os mesmos dados possam ser representados de forma menor. --- # 13. Saída do classificador A saída deve utilizar structured output. Contrato: ```json { "content_type": "text", "media_type": null } ``` ou: ```json { "content_type": "media", "media_type": "video" } ``` Schema conceitual: ```text content_type: text | media media_type: null | video | image | images | embed | mixed ``` Regras: ```text content_type = text → media_type obrigatoriamente null content_type = media → media_type obrigatoriamente diferente de null ``` Não adicionar: * confidence; * rationale; * summary; * keywords; * descrição gerada; * explicação do modelo. Esses campos não são necessários para a feature. --- # 14. Modelo primário O classificador primário será: ```text Qwen3.5 2B ``` executado localmente através de: ```text Ollama ``` O modelo deve utilizar configuração determinística compatível com o provider e structured output. Não utilizar reasoning desnecessário. --- # 15. Fallback de providers A ordem obrigatória é: ```text 1. Qwen3.5 2B / Ollama 2. GPT-OSS 20B / Groq 3. cgpt-web/gpt-5.5 / OmniRoute ``` O fallback é estritamente sequencial. --- # 16. Condição para fallback Fallback somente ocorre quando o provider atual falha operacionalmente. Exemplos: * conexão indisponível; * timeout; * erro HTTP; * provider indisponível; * resposta impossível de consumir; * resposta incompatível com o schema obrigatório. Não executar fallback porque: * a classificação foi `text`; * a classificação foi `media`; * o resultado parece estranho; * o modelo parece inseguro; * outro modelo poderia discordar. Não existe votação ou comparação entre providers. --- # 17. Falha de todos os providers Se: ```text Ollama ↓ falha Groq ↓ falha OmniRoute ↓ falha ``` o sistema não deve assumir: ```text text ``` nem: ```text media ``` O artigo deve receber: ```json { "classification_status": "failed", "error_message": "media classification providers unavailable" } ``` A mensagem concreta pode preservar o padrão de erro existente, desde que: * indique falha de classificação; * não exponha API keys; * não exponha tokens; * não exponha credenciais. O artigo: * não passa pelo multimotor; * não é enviado ao JSON de mídia; * permanece registrado no JSON principal de processamento como falha. --- # 18. Roteamento de conteúdo textual Se o resultado for: ```json { "content_type": "text", "media_type": null } ``` o pipeline continua exatamente pelo fluxo textual atual. A feature não deve modificar a lógica interna de: * Trafilatura; * Newspaper4k; * Readability; * seleção posterior dos resultados desses motores. --- # 19. Roteamento de conteúdo de mídia Se o resultado for: ```json { "content_type": "media", "media_type": "..." } ``` o artigo: 1. não executa Trafilatura; 2. não executa Newspaper4k; 3. não executa Readability; 4. é removido do fluxo textual; 5. é registrado no JSON separado de mídia. --- # 20. Arquivo de mídia Para uma entrada: ```text river_plate.json ``` o pipeline textual continua utilizando: ```text river_plate_extracted.json ``` e os conteúdos de mídia devem ser separados em: ```text river_plate_media.json ``` O arquivo deve ser JSON. Não utilizar XML. --- # 21. Dados mínimos no `*_media.json` Cada item deve preservar os dados básicos necessários para identificação do conteúdo e rastreabilidade do crawl. Estrutura mínima: ```json { "input_meta": { "titulo": "...", "subtitulo": "...", "quando_publicado": "...", "url": "...", "pagina": 1 }, "crawled_url": "...", "page_title": "...", "http_status": 200, "content_type": "media", "media_type": "video" } ``` Campos opcionais existentes no input continuam opcionais. Não adicionar dados de mídia extraídos. --- # 22. O que NÃO deve estar no arquivo de mídia O sistema não deve adicionar: ```text image_urls video_urls embed_urls thumbnails duration captions transcripts OCR alt-text gerado descrição da imagem provider da mídia metadata da mídia summary keywords sentiment ``` A extração da mídia pertence a outro sistema. --- # 23. Compatibilidade com o JSON textual atual O fluxo textual existente deve permanecer compatível com o contrato atual. A nova feature não exige mudanças nos objetos resultantes de artigos textuais processados com sucesso. Falhas de classificação devem reutilizar o arquivo principal de processamento e registrar explicitamente: ```text classification_status = failed ``` sem criar um terceiro arquivo físico exclusivo para falhas. --- # 24. Requisitos funcionais ## FR-001 O sistema deve analisar a DOM carregada antes de executar o multimotor textual. ## FR-002 A análise estrutural não pode usar regex. ## FR-003 Quando nenhuma mídia candidata estiver presente, o artigo deve seguir diretamente para o pipeline textual existente. ## FR-004 Quando existir mídia candidata, o sistema deve executar o classificador. ## FR-005 O classificador deve retornar somente `content_type` e `media_type`. ## FR-006 `content_type` deve aceitar apenas: ```text text media ``` ## FR-007 `media_type` deve aceitar apenas: ```text video image images embed mixed ``` ou `null` quando `content_type = text`. ## FR-008 Artigo classificado como `text` deve seguir pelo pipeline multimotor atual. ## FR-009 Artigo classificado como `media` não pode executar o multimotor. ## FR-010 Artigo classificado como `media` deve ser persistido no `*_media.json`. ## FR-011 O sistema não deve extrair mídia. ## FR-012 O sistema não deve navegar em carousel ou galeria. ## FR-013 O sistema não deve analisar arquivos de imagem, áudio ou vídeo. ## FR-014 O sistema deve utilizar Qwen3.5 2B/Ollama como classificador primário. ## FR-015 Falha operacional do Ollama deve acionar GPT-OSS 20B/Groq. ## FR-016 Falha operacional do Groq deve acionar `cgpt-web/gpt-5.5`/OmniRoute. ## FR-017 Fallback não pode ocorrer com base no conteúdo da classificação. ## FR-018 Falha dos três providers deve gerar `classification_status = failed`. ## FR-019 Falha dos três providers não pode gerar classificação presumida. ## FR-020 Falha dos três providers deve permanecer registrada no JSON principal. ## FR-021 O classificador deve funcionar independentemente do idioma do artigo. --- # 25. Requisitos não funcionais ## NFR-001 — Simplicidade Implementar a feature com o menor número razoável de componentes. ## NFR-002 — Isolamento A nova lógica deve ficar isolada do funcionamento interno dos extratores existentes. ## NFR-003 — Determinismo de contrato Toda resposta de modelo deve ser validada contra schema conhecido. ## NFR-004 — Configuração externa Endpoints, credenciais e configuração dos providers devem ser externos ao código. Segredos não podem ser hardcoded. ## NFR-005 — Segurança Logs e erros não podem registrar: * API keys; * tokens; * credenciais. ## NFR-006 — Compatibilidade A feature não deve quebrar o formato atual dos artigos textuais processados com sucesso. ## NFR-007 — Multilíngue Nenhuma lógica de decisão pode depender de listas de palavras em idiomas específicos. ## NFR-008 — Zero regex A nova implementação de detecção e classificação de mídia não pode utilizar bibliotecas ou APIs de expressão regular. ## NFR-009 — Observabilidade mínima Devem existir logs suficientes para identificar: * provider utilizado; * fallback Ollama → Groq; * fallback Groq → OmniRoute; * classificação final; * falha total da cadeia. Sem registrar payload textual integral por padrão. ## NFR-010 — Falha isolada Falha na classificação de um artigo não pode abortar o processamento dos demais artigos do lote. --- # 26. Caminho feliz — notícia textual ```text 1. Receber URL. 2. Crawler carrega página completamente. 3. Sistema analisa estruturalmente a DOM. 4. Nenhuma mídia candidata é encontrada OU o classificador determina que existe texto jornalístico substancial. 5. Artigo segue para extract_all_engines(). 6. Pipeline atual continua sem alteração. 7. Resultado permanece no *_extracted.json. ``` --- # 27. Caminho feliz — notícia de mídia ```text 1. Receber URL. 2. Crawler carrega página completamente. 3. Sistema encontra mídia candidata na DOM. 4. Qwen3.5 2B recebe representação compacta do conteúdo. 5. Modelo retorna: content_type = media media_type = images 6. Schema é validado. 7. Multimotor NÃO é executado. 8. Dados básicos do artigo são registrados no *_media.json. 9. Próximo artigo é processado. ``` --- # 28. Caminho de fallback ```text 1. Existe mídia candidata. 2. Ollama é chamado. 3. Ollama apresenta falha operacional. 4. Groq é chamado. 5. Groq apresenta falha operacional. 6. OmniRoute é chamado. 7. OmniRoute responde com classificação válida. 8. Resultado é utilizado normalmente. ``` Não existe qualquer diferença funcional entre uma classificação válida retornada pelo provider primário ou por um fallback. --- # 29. Caminho de falha total ```text 1. Existe mídia candidata. 2. Ollama falha. 3. Groq falha. 4. OmniRoute falha. 5. classification_status = failed. 6. Artigo NÃO passa pelo multimotor. 7. Artigo NÃO entra no *_media.json. 8. Falha é registrada no JSON principal. 9. Lote continua. ``` --- # 30. Cenários mínimos de teste ## Cenário A — texto sem mídia ```text texto jornalístico nenhuma mídia ``` Esperado: ```text multimotor executado ``` --- ## Cenário B — texto curto + vídeo Esperado: ```text media/video multimotor não executado ``` --- ## Cenário C — texto curto + imagem Esperado: ```text media/image ``` --- ## Cenário D — texto curto + múltiplas imagens Esperado: ```text media/images ``` --- ## Cenário E — texto curto + embed Esperado: ```text media/embed ``` --- ## Cenário F — texto curto + vídeo + imagens Esperado: ```text media/mixed ``` --- ## Cenário G — artigo textual longo + imagem Esperado: ```text text multimotor executado ``` --- ## Cenário H — artigo textual longo + vídeo Esperado: ```text text multimotor executado ``` --- ## Cenário I — Ollama indisponível Esperado: ```text Groq utilizado ``` --- ## Cenário J — Ollama e Groq indisponíveis Esperado: ```text OmniRoute utilizado ``` --- ## Cenário K — todos indisponíveis Esperado: ```text classification_status = failed nenhum multimotor nenhum registro em media.json lote continua ``` --- ## Cenário L — resposta fora do schema Esperado: ```text provider considerado incapaz de fornecer classificação válida seguir cadeia de fallback ``` --- # 31. Testes multilíngues O conjunto de testes/eval deve conter casos representativos em todos os idiomas efetivamente suportados pelo pipeline. O objetivo é validar especialmente: ```text texto curto descritivo + mídia → media texto jornalístico substancial + mídia → text ``` Não criar regras específicas por idioma para corrigir o modelo. --- # 32. Métricas operacionais Devem ser observáveis, no mínimo: ```text total de artigos avaliados total roteado para text total roteado para media total media/video total media/image total media/images total media/embed total media/mixed uso do fallback Groq uso do fallback OmniRoute falhas totais de classificação ``` Essas métricas podem utilizar o mecanismo de logging/relatório já existente. A feature não justifica introduzir uma nova plataforma de observabilidade. --- # 33. Fora de escopo Explicitamente fora desta feature: * extração das imagens; * extração dos vídeos; * download das mídias; * OCR; * visão computacional; * transcrição de vídeo; * transcrição de áudio; * identificação de objetos nas imagens; * identificação de infográfico; * geração de descrição; * geração de resumo; * NLP adicional; * interação com carousel; * chamadas às APIs das plataformas incorporadas; * classificação editorial da notícia; * verificação de fatos; * mudança no crawler; * mudança nos três motores atuais; * mudança no pipeline posterior de conteúdo textual; * armazenamento em banco; * filas; * workers novos; * agentes; * LangChain; * LangGraph; * votação de modelos; * scoring complexo; * classificação baseada em regex; * classificação baseada em palavras-chave por idioma. --- # 34. Definition of Done A feature está concluída quando: * [ ] classificação acontece antes do multimotor; * [ ] nenhuma regex é utilizada na nova lógica; * [ ] artigos sem mídia seguem normalmente; * [ ] artigos textuais com mídia continuam podendo ser classificados como `text`; * [ ] artigos predominantemente de mídia são identificados; * [ ] `video` funciona; * [ ] `image` funciona; * [ ] `images` funciona; * [ ] `embed` funciona; * [ ] `mixed` funciona; * [ ] artigos de mídia não executam os três motores; * [ ] `*_media.json` é produzido corretamente; * [ ] dados básicos do crawl são preservados; * [ ] nenhuma mídia é extraída; * [ ] Qwen3.5 2B/Ollama funciona como provider primário; * [ ] Groq funciona como primeiro fallback operacional; * [ ] OmniRoute funciona como segundo fallback operacional; * [ ] nenhum fallback ocorre com base em confiança ou discordância; * [ ] falha dos três providers é registrada como `classification_failed`; * [ ] falha de um artigo não interrompe o lote; * [ ] contrato estruturado do modelo é validado; * [ ] segredos não aparecem em logs; * [ ] testes cobrem os idiomas suportados; * [ ] comportamento atual do multimotor permanece compatível; * [ ] não foram adicionados componentes fora do escopo.