Files
TextNLPClassifierApp/README.md
T

716 lines
37 KiB
Markdown

# 🧠 TextNLPClassifierApp
[![Python Version](https://img.shields.io/badge/Python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue.svg)](https://www.python.org/)
[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Code Style](https://img.shields.io/badge/Code%20Style-Ruff-black.svg)](https://github.com/astral-sh/ruff)
[![Type Checked](https://img.shields.io/badge/Type%20Check-Mypy-blue.svg)](https://mypy-lang.org/)
[![Tests](https://img.shields.io/badge/Tests-Pytest%20(100%25%20Passing)-brightgreen.svg)](tests/)
> Plataforma modular em Python para **Classificação Multilíngue de Inerência de Entidades (NLP/LLM/ECP)**, **Extração Inteligente de Manchetes (Google News RSS & Foxcape)**, **Extração Multimotor de Artigos (Trafilatura, Newspaper4k, Readability)** e **Seleção Determinística de Conteúdo por Consenso Textual ($F_1$ Shingles)**.
---
## 📑 Tabela de Conteúdos
- [Visão Geral](#-visão-geral)
- [Instalação e Setup](#-instalação-e-setup)
- [1. Classificador de Conteúdo e Inerência (NLP / LLM / ECP)](#1--classificador-de-conteúdo-e-inerência-nlp--llm--ecp)
- [O que é e Como Funciona](#o-que-é-e-como-funciona)
- [Arquitetura de Classificação em 3 Tiers](#arquitetura-de-classificação-em-3-tiers)
- [Categorias de Decisão](#categorias-de-decisão)
- [Formato do ECP Snapshot e Markdown](#formato-do-ecp-snapshot-e-markdown)
- [Exemplos de Uso CLI](#exemplos-de-uso-cli)
- [2. Extrator de Manchetes do Google News](#2--extrator-de-manchetes-do-google-news)
- [O que é e Como Funciona](#o-que-é-e-como-funciona-1)
- [Diferenciais Técnicos](#diferenciais-técnicos)
- [Argumentos e Flags de Linha de Comando](#argumentos-e-flags-de-linha-de-comando)
- [Exemplos Práticos de Uso](#exemplos-práticos-de-uso)
- [3. Extrator e Parser Multimotor de Artigos](#3--extrator-e-parser-multimotor-de-artigos)
- [Visão Geral e Tríplice Extração](#visão-geral-e-tríplice-extração)
- [Argumentos e Flags CLI](#argumentos-e-flags-cli)
- [Exemplos de Uso](#exemplos-de-uso)
- [4. Seletor Determinístico de Conteúdo de Artigos](#4--seletor-determinístico-de-conteúdo-de-artigos)
- [Visão Geral e Algoritmo de Consenso ($F_1$)](#visão-geral-e-algoritmo-de-consenso-f_1)
- [Pipeline de Normalização e Shingles](#pipeline-de-normalização-e-shingles)
- [Critérios de Desempate Técnico e Resiliência](#critérios-de-desempate-técnico-e-resiliência)
- [Argumentos e Flags CLI](#argumentos-e-flags-cli-1)
- [Exemplos Práticos de Uso](#exemplos-práticos-de-uso-1)
- [5. Conversor de Artigo JSON para Markdown](#5--conversor-de-artigo-json-para-markdown)
- [Visão Geral e Estrutura do Documento](#visão-geral-e-estrutura-do-documento)
- [Isolamento Estrito de Extratores e Fallback](#isolamento-estrito-de-extratores-e-fallback)
- [Matriz Determinística de Metadados](#matriz-determinística-de-metadados)
- [Sanitização Editorial e Deduplicação](#sanitização-editorial-e-deduplicação)
- [Argumentos e Flags CLI](#argumentos-e-flags-cli-2)
- [Exemplos Práticos de Uso](#exemplos-práticos-de-uso-2)
- [6. Runtime de Consolidação e Higienização de Artigos (006-article-consolidation-runtime)](#6--runtime-de-consolidação-e-higienização-de-artigos-006-article-consolidation-runtime)
- [Visão Geral e Arquitetura](#visão-geral-e-arquitetura-do-runtime)
- [Configuração de Ambiente (.env)](#configuração-de-ambiente-env)
- [Comandos e Utilitários CLI](#comandos-e-utilitários-cli)
- [O que Esperar do Resultado (Artefatos Gerados)](#o-que-esperar-do-resultado-artefatos-gerados)
- [Contrato de Códigos de Saída (Exit Codes)](#contrato-de-códigos-de-saída-exit-codes)
- [Estrutura do Projeto](#-estrutura-do-projeto)
- [Testes e Qualidade de Código](#-testes-e-qualidade-de-código)
- [Licença](#-licença)
---
## 🌟 Visão Geral
O **TextNLPClassifierApp** reúne um ecossistema completo de ferramentas de engenharia de dados e processamento de linguagem natural:
1. **`classify.py`**: Motor de classificação semântica e contextual que determina o grau de aderência e inerência de um documento Markdown em relação a uma entidade alvo descrita em um **ECP Snapshot (Entity Context Profile)**.
2. **`scripts/extract_google_news.py`**: Extrator de notícias por palavra-chave, idioma e região geográfica utilizando navegação stealth **Foxcape** (headless), decodificação paralela de URLs para links reais e feedback em tempo real.
3. **`scripts/extract_article_contents.py`**: Extrator e parser de artigos multimotor com navegação stealth Foxcape headless e extração simultânea via **Trafilatura**, **Newspaper4k** (NLP) e **Readability**, consolidando texto higienizado, autores, datas, imagens e resumos.
4. **`scripts/select_article_extractor.py`**: Motor determinístico de seleção de extratores que avalia as saídas dos três motores, aplica normalização em memória, calcula métricas de consenso de shingles (5-tokens) com pontuação $F_1$, desempata tecnicamente ($\le 0.03$) favorecendo menor concisão/ruído e enriquece os dados de forma não-destrutiva e atômica.
5. **`scripts/convert_article_to_markdown.py`**: Conversor determinístico que recebe o JSON de um único artigo selecionado, isola estritamente o corpo do extrator vencedor (`trafilatura`, `newspaper4k` ou `readability`), resolve metadados editoriais por prioridade estrita, higieniza links/imagens/cabeçalhos e gera um documento Markdown (`.md`) padronizado com gravação atômica transacional.
---
## ⚙️ Instalação e Setup
### 1. Clonar o Repositório e Criar Ambiente Virtual
```bash
git clone <URL_DO_REPOSITORIO>
cd TextNLPClassifierApp
python -m venv .venv
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
# Linux/macOS
source .venv/bin/activate
```
### 2. Instalar Dependências
```bash
pip install -r requirements.txt
```
### 3. Baixar Binários do Navegador Stealth (Camoufox)
O motor **Foxcape** utiliza o navegador customizado Camoufox para evasão avançada de fingerprinting:
```bash
python -m camoufox fetch
```
---
## 1. 🧠 Classificador de Conteúdo e Inerência (NLP / LLM / ECP)
### O que é e Como Funciona
O classificador avalia se um texto em Markdown trata centralmente, contextualmente ou apenas de forma superficial de uma determinada entidade alvo (ex: empresa, figura pública, clube, conceito). A entidade é descrita através de um **ECP Snapshot (Entity Context Profile)** contendo nomes canônicos, apelidos (*aliases*), âncoras temáticas, âncoras negativas e entidades de contexto relacional.
O sistema suporta nativamente **6 idiomas**: Português (`pt`), Inglês (`en`), Espanhol (`es`), Francês (`fr`), Alemão (`de`) e Italiano (`it`).
### Arquitetura de Classificação em 3 Tiers
```mermaid
flowchart TD
MD[Markdown Content] --> Pre[Pré-processamento & Detecção de Idioma]
ECP[ECP Snapshot JSON] --> Pre
Pre --> T1[Tier 1: Determinístico & Regras NLP]
T1 -- Alta Confiança --> Decision[Decisão Final JSON]
T1 -- Score Intermediário / Ambíguo --> T2{Tier 2 Habilitado?}
T2 -- Sim --> Emb[Tier 2: Similaridade Semântica / Embeddings]
T2 -- Não --> Decision
Emb -- Inconclusivo --> T3{Tier 3 Habilitado?}
T3 -- Sim --> LLM[Tier 3: LLM Inherence Adapter]
T3 -- Não --> Decision
LLM --> Decision
```
* **Tier 1 (Determinístico / NLP Leve)**: Análise de frequência de termos, detecção de âncoras temáticas no primeiro terço do documento, contagem de aliases e penalização por âncoras negativas.
* **Tier 2 (Vetorial / Embeddings)** *(Opcional: `--enable-embeddings`)*: Projeção vetorial e cálculo de cosseno entre o perfil da entidade e os parágrafos do documento.
* **Tier 3 (LLM Fallback)** *(Opcional: `--enable-llm`)*: Adaptador de desambiguação inteligente (`src/adapters/llm.py`) acionado exclusivamente para casos limiares e ambíguos (ex: menção isolada `TANGENTIAL` ou confiança `< 0.60`). Casos claros não chamam o LLM para economizar custos e latência; em caso de falha de conexão com a API, degrada graciosamente mantendo o resultado do Tier 1 com aviso registrado em `warnings`.
### Categorias de Decisão
| Categoria | Descrição |
| :--- | :--- |
| `DIRECT_INHERENT` | O conteúdo é centrado e focado diretamente na entidade alvo. |
| `CONTEXTUAL_INHERENT` | A entidade é relevante no contexto da discussão, mesmo dividindo foco com outros temas. |
| `TANGENTIAL` | A entidade é mencionada apenas de passagem ou em listas ilustrativas. |
| `NOT_RELATED` | O conteúdo não possui relação substantiva com a entidade alvo. |
### Formato do ECP Snapshot e Markdown
#### Exemplo de ECP Snapshot Real (`examples/ecp_club_atletico_river_plate.json`):
```json
{
"target_entity_id": "ecp_river_plate",
"target_name": "Club Atlético River Plate",
"canonical_name": "Club Atlético River Plate",
"domain": "Futebol / Esportes",
"aliases": ["Club Atlético River Plate", "River Plate", "River", "El Millonario", "La Banda", "CARP"],
"anchors": ["Monumental", "Copa Libertadores", "Copa Sudamericana", "Eduardo Coudet", "Nicolás Otamendi"],
"negative_anchors": ["River Plate de Montevideo", "River Plate de Asunción", "Rio da Prata"],
"graph_version": "1.0.0",
"related_entities": [
{
"entity_id": "boca_juniors",
"name": "Club Atlético Boca Juniors",
"relation_type": "RIVAL_OF",
"weight": 0.9,
"aliases": ["Boca Juniors", "Boca", "Xeneize"]
},
{
"entity_id": "estadio_monumental",
"name": "Estadio Mâs Monumental",
"relation_type": "HOME_VENUE_OF",
"weight": 0.95,
"aliases": ["Monumental", "El Monumental"]
}
]
}
```
Perfis de exemplo prontos para uso em `examples/`:
- [`examples/ecp_club_atletico_river_plate.json`](examples/ecp_club_atletico_river_plate.json) (Club Atlético River Plate)
- [`examples/ecp_sao_paulo_futebol_clube.json`](examples/ecp_sao_paulo_futebol_clube.json) (São Paulo FC)
### Exemplos de Uso CLI
```bash
# Classificação Determinística padrão (Tier 1)
python classify.py \
--ecp examples/ecp_club_atletico_river_plate.json \
--content out/markdown/meu_artigo_001.md
# Salvar resultado em arquivo JSON formatado
python classify.py \
--ecp examples/ecp_club_atletico_river_plate.json \
--content out/markdown/meu_artigo_001.md \
--output out/classification_001.json
# Habilitar camadas adicionais (Embeddings e LLM)
python classify.py --ecp ecp.json --content artigo.md --enable-embeddings --enable-llm
```
---
## 2. 📰 Extrator de Manchetes do Google News
### O que é e Como Funciona
O script [`scripts/extract_google_news.py`](scripts/extract_google_news.py) é um extrator CLI autônomo projetado para consultar o feed RSS do Google News com máxima velocidade, resiliência e integridade de dados.
### Diferenciais Técnicos
* 🛡️ **Foxcape Headless Anti-Bot**: Utiliza o motor `foxcape` com `FoxcapeConfig(headless=True)` e Camoufox para evitar bloqueios 429/403 e captchas em segundo plano sem abrir navegadores visuais.
* 🔗 **Resolução Automática de URLs Reais**: Decodifica em paralelo (`ThreadPoolExecutor`) as URLs intermediárias do Google News (`news.google.com/rss/articles/...`) entregando diretamente o link final do portal de notícia (*Olé, TyC Sports, ge, ESPN, BBC, etc.*).
* 🧹 **Higienização de HTML**: Sanitiza resumos e descrições, eliminando tags HTML residuais (`<ol>`, `<li>`, `<a>`, `<span>`).
* 📊 **Logging Informativo no Terminal**: Emite o status passo a passo no canal `stderr` sem poluir a saída JSON em `stdout` (100% compatível com pipes e `jq`).
* 🌎 **Mapeamento de Idioma & Região**: Mapeia automaticamente códigos regionais (`pt` → `BR:pt-BR`, `es` → `AR:es-419`, `en` → `US:en-US`) permitindo customização com `--locale`.
### Argumentos e Flags de Linha de Comando
| Flag | Tipo | Obrigatório | Padrão | Descrição |
| :--- | :--- | :--- | :--- | :--- |
| `-q`, `--query`, `--keyword` | `str` | **Sim** | — | Termo de pesquisa (ex: `"River Plate"`). |
| `-l`, `--lang`, `--language` | `str` | Não | `"pt"` | Código do idioma (`pt`, `es`, `en`, `de`, `fr`, `it`). |
| `--locale`, `--country` | `str` | Não | `None` | País/região editorial (`BR`, `AR`, `MX`, `US`, `GB`, `ES`). |
| `-p`, `--max-pages` | `int` | Não | `1` | Quantidade de páginas (1 a 10; cada página traz 10 itens). |
| `-o`, `--output` | `str` | Não | `None` | Caminho para salvar o arquivo JSON. |
| `--pretty` | `flag` | Não | `False` | Formata o JSON no terminal com indentação de 2 espaços. |
| `--no-resolve-urls` | `flag` | Não | `False` | Desativa a resolução e mantém as URLs brutas do Google News. |
| `-s`, `--silent`, `--quiet` | `flag` | Não | `False` | Suprime os logs de progresso no `stderr`. |
### Exemplos Práticos de Uso
```bash
# River Plate (Argentina / Espanhol / 2 Páginas / Salvar em Arquivo)
python scripts/extract_google_news.py -q "River Plate" -l es --locale AR -p 2 -o out/river_plate.json
# Cruzeiro (Brasil / Português / Formatado no Terminal)
python scripts/extract_google_news.py --query "Cruzeiro" --lang pt --locale BR --pretty
# Filtragem com jq em modo silencioso
python scripts/extract_google_news.py -q "inteligência artificial" -s | jq '.items[].url'
```
---
## 3. 📄 Extrator e Parser Multimotor de Artigos
### Visão Geral e Tríplice Extração
O script [`scripts/extract_article_contents.py`](scripts/extract_article_contents.py) lê os arquivos JSON gerados pelo extrator do Google News (ou qualquer lista contendo `items` com `url`), acessa cada página via **Foxcape** em modo stealth headless (reutilizando uma única sessão de navegador ativa com espera do evento `domcontentloaded`), e executa simultaneamente 3 motores especializados de extração:
1. **Trafilatura**: Texto principal higienizado, autores, data de publicação, categorias, tags, URL canônica e payload estruturado nativo.
2. **Newspaper4k**: Artigo completo, autores, imagens (`top_image` e galeria), resumo automático e palavras-chave (*keywords*) extraídas por NLP nativo.
3. **Readability (`readability-lxml`)**: Miolo limpo em HTML sem anúncios ou scripts supérfluos, títulos e texto puro formatado.
O JSON final consolidado é salvo em `out/` com descarte de strings HTML brutas para manter o arquivo leve e veloz.
### Argumentos e Flags CLI
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
| `-i, --input` | Caminho (obrigatório) | — | Arquivo JSON de busca de notícias (ex: `out/river_plate.json`). |
| `-o, --output` | Caminho (opcional) | `<input_stem>_extracted.json` | Arquivo JSON de destino consolidado. |
| `-l, --limit` | Inteiro (opcional) | Todos | Limita a quantidade máxima de notícias processadas. |
| `--lang, --language` | String (opcional) | Do JSON / `en` | Sobrescreve o código de idioma para o NLP do Newspaper4k (ex: `pt`, `es`, `en`). |
| `-t, --timeout` | Inteiro (opcional) | `30` | Timeout em segundos por página no Foxcape. |
| `-s, --silent` | Flag booleana | `False` | Suprime logs informativos de progresso no `stderr`. |
### Exemplos de Uso
```bash
# Extração Completa Automática (gera out/river_plate_extracted.json)
python scripts/extract_article_contents.py -i out/river_plate.json
# Amostragem Rápida (Limit 2 Notícias)
python scripts/extract_article_contents.py -i out/river_plate.json --limit 2
# Destino Customizado e Timeout Ajustado
python scripts/extract_article_contents.py -i out/petrobras_result.json -o out/petrobras_full.json --timeout 45
```
---
## 4. 🎯 Seletor Determinístico de Conteúdo de Artigos
### Visão Geral e Algoritmo de Consenso ($F_1$)
O script [`scripts/select_article_extractor.py`](scripts/select_article_extractor.py) é uma ferramenta autônoma, leve e 100% determinística (baseada exclusivamente na biblioteca padrão do Python) que resolve o problema de divergência entre múltiplos extratores de texto.
O motor analisa as saídas dos candidatos ativos (`trafilatura`, `newspaper4k` e `readability`), compara a sobreposição de conteúdo através de **shingles de 5 tokens** e calcula o índice de concordância mútua via pontuação $F_1$:
$$\text{Coverage} = \frac{|\text{Shingles do Candidato} \cap \text{Consenso}|}{|\text{Consenso}|}$$
$$\text{Support} = \frac{|\text{Shingles do Candidato} \cap \text{Consenso}|}{|\text{Shingles do Candidato}|}$$
$$F_1 = \frac{2 \times \text{Coverage} \times \text{Support}}{\text{Coverage} + \text{Support}}$$
```mermaid
flowchart TD
Art[Artigo com Trafilatura, Newspaper4k e Readability] --> Class[Classificação de Viabilidade: Usable, Degraded, Unavailable]
Class --> Set[Formação do Conjunto Ativo]
Set --> Norm[Normalização NFKC & Shingles de 5 Tokens]
Norm --> Metric[Cálculo de Consenso & Métricas F1]
Metric --> Check{Existe Consenso >= 2?}
Check -- Sim --> TopScore[Avaliação do Top Score]
TopScore --> TiePool{Empate Técnico <= 0.03?}
TiePool -- Sim --> SmallestShingle[Menor Quantidade de Shingles / Menos Ruído]
SmallestShingle --> Winner[Extrator Selecionado]
TiePool -- Não --> HighestScore[Maior Pontuação F1]
HighestScore --> Winner
Check -- Não --> ZeroCons[Desempate Sem Consenso: Mediana / Max / Prioridade]
ZeroCons --> Winner
```
### Pipeline de Normalização e Shingles
A normalização ocorre em memória exclusivamente para fins comparativos:
1. **Decodificação de entidades HTML** (`html.unescape`).
2. **Descarte de imagens Markdown** (`![alt](url)`) para evitar que descrições de imagens criem falsos consensos com legendas.
3. **Preservação de links Markdown** (`[texto](url)` $\to$ `texto`).
4. **Remoção de tags HTML** preservando espaçamento entre palavras adjacentes.
5. **Normalização Unicode NFKC** e conversão para minúsculas.
6. **Colapso de espaços em branco**.
7. **Tokenização Unicode alfanumérica** com descarte de pontuações.
8. **Geração de Shingles**: Janela deslizante de 5 tokens (ou tupla única para textos curtos de 1 a 4 tokens).
### Critérios de Desempate Técnico e Resiliência
* **Empate Técnico ($\le 0.03$)**: Quando dois ou mais extratores atingem pontuações com diferença $\le 0.03$, o algoritmo seleciona aquele com **menor quantidade de shingles** (penalizando *boilerplate*, cabeçalhos ou menus excedentes).
* **Desempate Hierárquico Estrito**: Em caso de empate absoluto de pontuação e tamanho, aplica-se a hierarquia fixa:
$$\text{newspaper4k} > \text{readability} > \text{trafilatura}$$
* **Cenários de 0 Consenso**:
* 3 candidatos ativos $\to$ seleciona o de **tamanho mediano de shingles**.
* 2 candidatos ativos $\to$ seleciona o de **maior tamanho de shingles**.
* 1 candidato ativo $\to$ seleciona o único utilizável.
* Todos indisponíveis $\to$ *fallback* obrigatório em `newspaper4k`.
* **Gravação Atômica e Não-Destrutiva**: Criação de arquivo temporário com substituição atômica (`os.replace`), preservando 100% dos dados pré-existentes, ordem de artigos e propriedades originais.
### Argumentos e Flags CLI
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
| `input_file` | Posicional (obrigatório) | — | Caminho para o arquivo JSON contendo a coleção `articles`. |
| `-o, --output` | Caminho (opcional) | `<input_stem>_selected.json` | Caminho do arquivo JSON de destino. |
| `--indent` | Inteiro (opcional) | `2` | Espaços de indentação do JSON (`0` para compacto). |
| `-v, --verbose` | Flag booleana | `False` | Emite no `stderr` os detalhes de pontuação, ativos e regra de escolha por artigo. |
### Exemplos Práticos de Uso
#### 1. Execução Padrão Automática
```bash
python scripts/select_article_extractor.py out/river_plate_extracted.json
# Gera automaticamente out/river_plate_extracted_selected.json
```
#### 2. Execução com Modo Verboso
```bash
python scripts/select_article_extractor.py out/river_plate_extracted.json --verbose
```
* **Saída no Terminal**:
```text
[Artigo #001] Extrator: newspaper4k | Motivo: technical_tie_smallest_shingles | Ativos: 3 | Consenso: 1048
[Artigo #002] Extrator: newspaper4k | Motivo: highest_score | Ativos: 3 | Consenso: 333
[Artigo #003] Extrator: readability | Motivo: highest_score | Ativos: 3 | Consenso: 778
...
{
"status": "success",
"input_file": "out/river_plate_extracted.json",
"output_file": "out/river_plate_extracted_selected.json",
"total_articles": 20,
"processed_count": 20,
"distribution": {
"newspaper4k": 9,
"readability": 9,
"trafilatura": 2
}
}
```
#### 3. Uso Programático como Módulo Python
```python
from scripts.select_article_extractor import select_article_extractor
article_data = {
"trafilatura": {"text": "River Plate venceu ontem por 3-0.", "error": None},
"newspaper4k": {"text": "River Plate venceu ontem por 3-0 no Monumental.", "error": None},
"readability": {"cleaned_text": "River Plate venceu ontem por 3-0.", "error": None},
}
result = select_article_extractor(article_data)
print("Extrator Selecionado:", result.selected_extractor.value)
print("Motivo:", result.selection_reason)
```
---
## 5. 📝 Conversor de Artigo JSON para Markdown
### Visão Geral e Estrutura do Documento
O script `scripts/convert_article_to_markdown.py` realiza a conversão de um arquivo JSON contendo exatamente um artigo (com `selected_extractor`) para um documento Markdown (`.md`) pronto para consumo editorial ou classificação downstream.
A estrutura do Markdown gerado segue estritamente o padrão:
```markdown
# Título do Artigo
Subtítulo ou descrição editorial (omitido se ausente ou igual ao título).
**Autor:** Nome do Autor 1, Nome do Autor 2
**Publicado em:** 2026-08-20T00:36:33-03:00
**Site:** Nome do Veículo
**Categoria:** Categoria 1, Categoria 2
**Tags:** Tag 1, Tag 2
**Palavras-chave:** Palavra 1, Palavra 2
**Idioma:** es
**Fonte original:** [https://exemplo.com/artigo](https://exemplo.com/artigo)
![Imagem principal](https://exemplo.com/imagem_capa.jpg)
---
Conteúdo textual do artigo convertido em Markdown.
```
### Isolamento Estrito de Extratores e Fallback
- **Isolamento Total do Corpo**: O texto e HTML do corpo vêm **exclusivamente** do extrator indicado em `selected_extractor`. Caso o extrator selecionado não possua corpo válido, o processo encerra imediatamente com erro (código `1`). Nunca ocorre fallback de corpo para outro extrator.
- **Fallback Interno**:
- `trafilatura`: usa `trafilatura.markdown`; se vazio, usa `trafilatura.text`.
- `newspaper4k`: converte `newspaper4k.article_html` para Markdown; se vazio, usa `newspaper4k.text`.
- `readability`: converte `readability.cleaned_html` para Markdown; se vazio, usa `readability.cleaned_text`.
- **Conversão HTML→Markdown**: Utiliza a biblioteca `markdownify` configurada para títulos padrão ATX (`#`, `##`, `###`), preservando negrito, itálico, listas, tabelas, citações, links e blocos de código.
### Matriz Determinística de Metadados
Os metadados editoriais são resolvidos deterministicamente consultando fontes na ordem de prioridade estrita:
1. **Título**: `SELECIONADO.title` → `input_meta.titulo` → `page_title` → `newspaper4k.title` → `trafilatura.title` → `readability.title`
2. **URL Original**: `input_meta.url` → `crawled_url` → URL canônica do selecionado → `trafilatura.canonical_url` → `newspaper4k.canonical_link`
3. **Subtítulo/Descrição**: descrição do selecionado → `trafilatura.description` → `newspaper4k.meta_description` → `input_meta.subtitulo`
4. **Autores**: autor(es) do selecionado → `newspaper4k.authors` → `trafilatura.author` → `readability.author`
5. **Data de Publicação**: data do selecionado → `newspaper4k.publish_date` → `trafilatura.date` → `input_meta.quando_publicado`
6. **Site**: site do selecionado → `trafilatura.sitename` → `newspaper4k.meta_site_name` → `trafilatura.hostname` → Hostname da URL original
7. **Categorias**: categorias do selecionado → `trafilatura.categories`
8. **Tags**: tags do selecionado → `trafilatura.tags` → `newspaper4k.tags` → `newspaper4k.meta_keywords`
9. **Palavras-chave**: `newspaper4k.keywords` → `newspaper4k.meta_keywords`
10. **Idioma**: idioma do selecionado → `trafilatura.language` → `newspaper4k.meta_lang`
11. **Imagem Principal**: imagem do selecionado → `newspaper4k.top_image` → `trafilatura.image`
### Sanitização Editorial e Deduplicação
- **Remoção de H1 Duplicado**: Se o corpo iniciar com um título H1 idêntico ao título resolvido do artigo, esse H1 inicial é removido automaticamente.
- **Filtro de Imagens**: Remove imagens com URLs relativas, vazias ou em formato `data:`. Deduplica URLs de imagem repetidas no corpo.
- **Filtro de Placeholders**: Descarta valores como `null`, `None`, `N/A`, `unknown`, `[no-author]` ou rótulos vazios.
- **Gravação Atômica**: Escrita transacional em arquivo temporário seguida de substituição com `os.replace`, garantindo integridade e nenhum resíduo em falhas.
### Argumentos e Flags CLI
| Parâmetro | Tipo | Obrigatoriedade | Padrão | Descrição |
|---|---|:---:|---|---|
| `-i, --input` | Caminho | **Sim** | — | Arquivo JSON contendo exatamente um único artigo. |
| `-o, --output` | Caminho | Não | `<input_stem>.md` | Caminho do arquivo Markdown de destino. |
### Exemplos Práticos de Uso
#### 1. Conversão Padrão
```bash
python scripts/convert_article_to_markdown.py -i out/article_001.json
# Gera automaticamente out/article_001.md
```
#### 2. Conversão com Caminho de Destino Personalizado
```bash
python scripts/convert_article_to_markdown.py \
-i out/article_001.json \
-o out/markdown/meu_artigo.md
```
#### 3. Uso Programático em Python
```python
from pathlib import Path
from scripts.convert_article_to_markdown import convert_article
out_file = convert_article(Path("out/article_001.json"), Path("out/artigo.md"))
print(f"Markdown gerado em: {out_file}")
```
---
## 6. 🚀 Runtime de Consolidação e Higienização de Artigos (006-article-consolidation-runtime)
### Visão Geral e Arquitetura do Runtime
O **Runtime de Consolidação e Higienização Editorial de Artigos** é um pipeline de produção industrial de alta confiabilidade projetado para transformar a saída de extração tríplice de artigos em documentos editoriais finais em **Markdown limpo e estruturado com Manifesto JSON de auditoria completa**, operando estritamente sob **modelos baratos de IA (Groq / DeepSeek / OpenAI / Proxies)** e com **proibição total de expressões regulares (`zero-regex`)** em suas operações de texto.
```mermaid
flowchart TD
In[Artigo JSON + ECP Snapshot] --> Val[1. Validação Estrita de Schemas e Preflight]
Val --> FP[2. Cálculo de Fingerprint Determinístico SHA-256]
FP --> SQLClaim[3. Claim Atômico no SQLite WAL - BEGIN IMMEDIATE]
SQLClaim --> Shingles[4. Parser de Candidatos e Mapeamento de Equivalências sem Regex]
Shingles --> HygLLM[5. Higienização Extrativa por LLM - Grounding por IDs de Blocos]
HygLLM --> RepVal[6. Validador de Reparos Textuais Restritos - Mojibake/Typos/Espaçamento]
RepVal --> ECPGate{7. Gate Obrigatório de ECP}
ECPGate -- Não Inerente / Tangencial --> RejManifest[Grava Manifesto rejected_ecp - Zero Markdown]
ECPGate -- Inerente DIRECT/CONTEXTUAL --> EnrichLLM[8. Enriquecimento LLM - Sentimento e Tags]
EnrichLLM --> AtomPersist[9. Persistência Atômica temp + os.replace]
AtomPersist --> OutMD[10. Markdown com YAML Front-matter + Manifesto .result.json]
OutMD --> SQLiteDone[11. Atualização de Estado completed_text no SQLite]
```
### Configuração de Ambiente (`.env`)
Crie ou edite o arquivo `.env` na raiz do projeto com suas credenciais:
```bash
# Provedor Padrão (OpenAI / Omniroute / Proxy Customizado)
OPENAI_API_KEY="sk-..."
OPENAI_BASE_URL="https://omniroute.app.andreferraro.com/v1"
OPENAI_MODEL="cgpt-web/gpt-5.5"
# Ou Provedores Nativos Específicos
GROQ_API_KEY="gsk_..."
DEEPSEEK_API_KEY="sk-..."
# Observabilidade (Opcional - Degrada para fila SQLite local se offline)
LANGFUSE_PUBLIC_KEY="pk-lf-..."
LANGFUSE_SECRET_KEY="sk-lf-..."
LANGFUSE_HOST="https://cloud.langfuse.com"
```
---
### Comandos e Utilitários CLI
O runtime expõe **5 utilitários CLI normativos**:
#### 1. Consolidação de Artigo Único (`consolidate.py`)
Executa a consolidação de ponta a ponta de um artigo contra um perfil de entidade (ECP):
```bash
python src/runtime/cli/consolidate.py \
--config runtime_config.local.json \
--article examples/sample_article_valid.json \
--ecp examples/sample_ecp_snapshot.json
```
#### 2. Certificação de Pré-Voo (`preflight.py`)
Valida permissões de disco, banco de dados SQLite, prompts, hashes e integridade do ambiente antes do início das operações:
```bash
python src/runtime/cli/preflight.py --config runtime_config.local.json
```
#### 3. Teste de Fumaça (`smoke.py`)
Valida rapidamente o funcionamento do pipeline completo com dados de exemplo locais:
```bash
python src/runtime/cli/smoke.py --config runtime_config.local.json
```
#### 4. Reconciliação e Recuperação de Quedas (`reconcile.py`)
Detecta artefatos gravados em disco e reconcilia o estado do banco SQLite após reinicializações ou crashes do processo:
```bash
# Auditoria e sincronização de estados divergentes
python src/runtime/cli/reconcile.py --config runtime_config.local.json
# Reconciliação com limpeza de arquivos temporários órfãos (.tmp_*)
python src/runtime/cli/reconcile.py --config runtime_config.local.json --cleanup-orphans
```
#### 5. Despejo de Telemetria Operacional (`telemetry_flush.py`)
Efetua o flush em lote de eventos e métricas enfileirados localmente no SQLite quando a conexão com o Langfuse foi restabelecida:
```bash
python src/runtime/cli/telemetry_flush.py --config runtime_config.local.json --batch-size 50
```
---
### O que Esperar do Resultado (Artefatos Gerados)
Para cada artigo processado, o runtime grava seus artefatos no diretório configurado (ex: `out/articles/`):
#### 1. Documento Markdown Higienizado (`<fingerprint>.md`)
Quando o artigo é classificado como inerente (`DIRECT_INHERENT` ou `CONTEXTUAL_INHERENT`), um arquivo Markdown padronizado é gerado com **YAML Front-Matter** estrito e corpo textual higienizado:
```markdown
---
title: "Los puntajes de River vs. Independiente Santa Fe"
fingerprint: "c987f362b35e76239e3fda0841c9e5f89c3d4193760738e6a2e9424ce45fde88"
source_url: "https://www.tycsports.com/river-plate/los-puntajes-de-river-vs-independiente-santa-fe.html"
sentiment: "positive"
---
# Los puntajes de River vs. Independiente Santa Fe
River Plate empató sin goles ante Independiente Santa Fe en el estadio El Campín de Bogotá por la ida de los octavos de final de la Copa Sudamericana.
El equipo de Marcelo Gallardo resistió la presión del conjunto colombiano y definirá la serie la próxima semana en el estadio Monumental de Buenos Aires.
```
#### 2. Manifesto de Auditoria e Resultado (`<fingerprint>.result.json`)
Contém a auditoria completa de hashes, tokens, latência, custos, status ECP e metadados:
```json
{
"schema_version": "1.0.0",
"fingerprint": "c987f362b35e76239e3fda0841c9e5f89c3d4193760738e6a2e9424ce45fde88",
"source_url": "https://www.tycsports.com/river-plate/los-puntajes-de-river-vs-independiente-santa-fe.html",
"selected_extractor": "trafilatura",
"final_status": "completed_text",
"generate_markdown": true,
"markdown_path": "out/articles/c987f362b35e76239e3fda0841c9e5f89c3d4193760738e6a2e9424ce45fde88.md",
"markdown_hash": "b6b21b19f10feab12525030016a3eeb4ed702cdec6d39c91fc42289b65091e0e",
"ecp_classification": {
"category": "DIRECT_INHERENT",
"is_inherent": true,
"confidence": 0.98,
"rationale": "Direct match of target entity 'Club Atlético River Plate' with strong contextual anchor density (4 anchor(s) matched)."
},
"enrichment": {
"sentiment": "positive",
"tags": ["river plate", "copa sudamericana", "futebol"]
},
"config_version": "1.0.0",
"error_codes": []
}
```
> **Nota sobre Artigos Não Inerentes**: Caso o artigo seja rejeitado pelo ECP (`TANGENTIAL` ou `NOT_RELATED`), o arquivo `.md` **não é gerado** (zero bytes de lixo editorial) e o manifesto `.result.json` é gravado com `final_status: "rejected_ecp"` e `generate_markdown: false`.
#### 3. Rastreamento e Estado no Banco SQLite (`out/runtime.db`)
O banco SQLite opera em modo **WAL** com transações imediatas para prevenir concorrência e deadlocks, registrando o histórico de transições de status (`received` → `claimed` → `hygiene_running` → `ecp_evaluated` → `enrichment_running` → `completed_text` / `rejected_ecp`).
---
### Contrato de Códigos de Saída (Exit Codes)
O CLI segue estritamente os códigos de saída normativos:
| Código | Significado | Descrição |
| :---: | :--- | :--- |
| `0` | **Sucesso** | Processamento completado com sucesso (artigo consolidado ou rejeitado com manifesto válido). |
| `1` | **Erro de Contrato / Input** | JSON de entrada inválido, schema corrompido ou argumentos ausentes. |
| `2` | **Erro de Preflight / Config** | Arquivo de configuração ausente, chave de API inexistente ou modelo não certificado. |
| `3` | **Erro de Gateway / Fallback** | Provedor primário e fallback falharam simultaneamente sem recuperação determinística. |
| `4` | **Erro Fatal de I/O** | Disco inacessível, falha de integridade SHA-256 ou corrupção de persistência atômica. |
---
## 📁 Estrutura do Projeto
```text
TextNLPClassifierApp/
├── runtime_config.local.json # Configuração canônica do Runtime (roles, modelos, pricing)
├── .env # Chaves de API e URLs locais (gitignored)
├── classify.py # CLI legado do Classificador de Inerência
├── scripts/
│ ├── __init__.py # Pacote utilitário de scripts
│ ├── ci_check.py # Pipeline unificado de validação estática e CI
│ ├── extract_google_news.py # CLI de Extração de Manchetes do Google News
│ ├── extract_article_contents.py # CLI de Extração e Parsing Multimotor de Artigos
│ ├── select_article_extractor.py # CLI de Seleção Determinística de Extrator
│ └── convert_article_to_markdown.py # CLI de Conversão de Artigo JSON para Markdown
├── src/
│ ├── runtime/ # MÓDULOS CENTRAIS DO RUNTIME DE CONSOLIDAÇÃO
│ │ ├── candidate/ # Parser de candidatos, shingles e normalização zero-regex
│ │ ├── cli/ # CLIs: consolidate, preflight, smoke, reconcile, flush
│ │ ├── core/ # Configs, contratos, fingerprint SHA-256 e limites
│ │ ├── ecp/ # Adapter de decisão de inerência e schema ECP
│ │ ├── enrichment/ # Harness de enriquecimento (sentimento e tags)
│ │ ├── gateway/ # Gateway agnóstico (Groq, DeepSeek, OpenAI) com failover
│ │ ├── hygiene/ # Harness de higienização LLM e grounding por IDs
│ │ ├── observability/ # Logging estruturado e tracer Langfuse com fila offline
│ │ ├── quality/ # Validador de reparos textuais restritos (mojibake/typos)
│ │ └── storage/ # Persistência atômica (file_store) e SQLite WAL
│ └── tools/ # Módulos e extratores legados (classifier, language, parser)
├── specs/ # Especificações Speckit (001 a 006)
│ └── 006-article-consolidation-runtime/ # Especificação técnica completa do runtime
├── docs/structured_extraction/ # Documentação arquitetural (PRD, ADRs, Test Plan, Runbook)
├── tests/
│ ├── runtime/ # SUÍTE DO RUNTIME (72 testes)
│ │ ├── contract/ # Validação de schemas e contratos
│ │ ├── fault_injection/ # Falhas HTTP 429, 500, JSON corrompido
│ │ ├── integration/ # Concorrência (8 workers), Reconciliação, CLI subprocess, E2E Real
│ │ ├── load/ # Benchmark de sustentação de carga (100 art/h)
│ │ ├── quality/ # Orçamento de custo, Golden Set 20, Zero Regex scanner
│ │ ├── security/ # Injeção de prompt e redação de secrets
│ │ └── unit/ # Testes unitários dos módulos internos
│ ├── tools/ # SUÍTE DE TOOLS LEGADAS (247 testes)
│ └── scripts/check_zero_regex.py # Auditor estático de AST garantindo Zero Regex
├── requirements.txt # Dependências do projeto
├── pyproject.toml # Configurações de ferramentas (pytest, ruff, mypy)
└── README.md # Documentação principal
```
---
## 🧪 Testes e Qualidade de Código
O repositório possui **319 testes automatizados** com **100% de aprovação**:
```bash
# 1. Executar a Verificação Completa do CI (Validação Estática Zero-Regex + 319 Testes)
python scripts/ci_check.py
# 2. Executar apenas a Suíte do Runtime (72 testes)
pytest tests/runtime -v
# 3. Executar o Teste E2E Real contra API ao vivo
pytest tests/runtime/integration/test_live_e2e_real_api.py -v -s
# 4. Executar o Teste de Concorrência de Alta Contenção (8 workers paralelos simultâneos)
pytest tests/runtime/integration/test_concurrency_claims.py -v
# 5. Executar apenas a Suíte de Ferramentas Legadas (247 testes)
pytest tests/tools -v
# 6. Auditoria Estática de Proibição Absoluta de Regex
python tests/scripts/check_zero_regex.py
```
---
## 📄 Licença
Este projeto está licenciado sob os termos da licença **MIT**. Consulte o arquivo `LICENSE` para mais detalhes.