feat: add deterministic content extractor selector engine with F1 consensus

This commit is contained in:
2026-08-20 22:09:43 -03:00
parent 6a45368cb0
commit ff7a50e0eb
46 changed files with 18503 additions and 2813 deletions
+148 -42
View File
@@ -6,7 +6,7 @@
[![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)** e **Extração Inteligente de Manchetes de Notícias com Evasão Anti-Bot (Google News RSS & Foxcape)**.
> 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)**.
---
@@ -29,6 +29,12 @@
- [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)
- [Estrutura do Projeto](#-estrutura-do-projeto)
- [Testes e Qualidade de Código](#-testes-e-qualidade-de-código)
- [Licença](#-licença)
@@ -37,11 +43,12 @@
## 🌟 Visão Geral
O **TextNLPClassifierApp** reúne ferramentas de engenharia de dados e processamento de linguagem natural:
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 definida 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 que utiliza o motor stealth **Foxcape** (em modo headless), decodificação paralela de URLs para os links reais dos portais de notícias 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 combinada via **Trafilatura**, **Newspaper4k** (NLP) e **Readability**, consolidando texto higienizado, autores, datas, imagens e resumos em JSON estruturado.
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.
---
@@ -157,7 +164,7 @@ python classify.py --ecp ecp.json --content artigo.md --enable-embeddings --enab
### O que é e Como Funciona
O script [`scripts/extract_google_news.py`](file:///c:/Users/aferr/Projects/AFTech/DunaMedia/TextNLPClassifierApp/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.
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
@@ -182,42 +189,24 @@ O script [`scripts/extract_google_news.py`](file:///c:/Users/aferr/Projects/AFTe
### Exemplos Práticos de Uso
#### 1. River Plate (Argentina / Espanhol / 2 Páginas / Salvar em Arquivo)
```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
```
* **Saída no Terminal**:
```text
[INFO] 🔍 Consultando Google News: 'River Plate' (idioma: es, locale: AR, max_pages: 2)...
[INFO] 📥 Feed RSS recebido (162117 bytes).
[INFO] 📰 20 artigos extraídos do feed XML.
[INFO] 🔗 Decodificando 20 URLs do Google News para os portais reais...
[INFO] ✅ 20/20 URLs resolvidas com sucesso para os domínios de origem.
[INFO] 💾 Arquivo salvo com sucesso: 'out/river_plate.json' (20 notícias).
```
#### 2. Cruzeiro (Brasil / Português / Formatado no Terminal)
```bash
# Cruzeiro (Brasil / Português / Formatado no Terminal)
python scripts/extract_google_news.py --query "Cruzeiro" --lang pt --locale BR --pretty
```
#### 3. Fórmula 1 (Inglaterra / Inglês)
```bash
python scripts/extract_google_news.py --query "Formula 1" --lang en --locale GB --pretty
```
#### 4. Filtragem com `jq` em Modo Silencioso
```bash
# 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
## 3. 📄 Extrator e Parser Multimotor de Artigos
### Visão Geral e Tríplice Extração
O script `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:
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.
@@ -238,20 +227,128 @@ O JSON final consolidado é salvo em `out/` com descarte de strings HTML brutas
### Exemplos de Uso
#### 1. Extração Completa Automática
```bash
# Extração Completa Automática (gera out/river_plate_extracted.json)
python scripts/extract_article_contents.py -i out/river_plate.json
# Gera automaticamente out/river_plate_extracted.json
```
#### 2. Amostragem Rápida (Limit 2 Notícias)
```bash
# 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
```
#### 3. Destino Customizado e Timeout Ajustado
---
## 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/extract_article_contents.py -i out/petrobras_result.json -o out/petrobras_full.json --timeout 45
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)
```
---
@@ -264,7 +361,8 @@ TextNLPClassifierApp/
├── scripts/
│ ├── __init__.py # Pacote utilitário de scripts
│ ├── 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
│ ├── 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
├── src/ # Módulos centrais do classificador
│ ├── classifier.py # Orquestrador de classificação (Tier 1, 2, 3)
│ ├── models.py # Modelos de dados e esquemas (ECPSnapshot, Decision)
@@ -273,11 +371,13 @@ TextNLPClassifierApp/
├── specs/ # Especificações e planos arquiteturais (Speckit)
│ ├── 001-multilingual-entity-classifier/
│ ├── 002-google-news-extractor/
│ └── 003-article-content-extractor/ # Specs da feature de extração multimotor
│ ├── 003-article-content-extractor/
│ └── 004-deterministic-content-selection/ # Specs da seleção determinística
├── tests/ # Suíte de testes automatizados
│ ├── test_classifier.py
│ ├── test_extract_google_news.py
│ └── test_extract_article_contents.py # Testes do extrator de conteúdo
│ ├── test_extract_article_contents.py
│ └── test_select_article_extractor.py # Testes do seletor determinístico
├── requirements.txt # Dependências do projeto
├── pyproject.toml # Configurações de ferramentas (pytest, ruff, mypy)
└── README.md # Documentação principal
@@ -287,13 +387,19 @@ TextNLPClassifierApp/
## 🧪 Testes e Qualidade de Código
O repositório possui cobertura com testes unitários, testes de integração e testes End-to-End (E2E) com requisição de rede ao vivo:
O repositório possui **120 testes automatizados** com 100% de aprovação cobrindo testes unitários, de regressão, de integração e testes End-to-End (E2E) via CLI subprocess:
```bash
# Executar todos os testes do projeto
# Executar toda a suíte de testes do projeto (120 testes)
pytest -v
# Executar especificamente os testes do Extrator de Notícias
# Executar especificamente os testes do Seletor Determinístico
pytest tests/test_select_article_extractor.py -v
# Executar os testes do Extrator de Conteúdo Multimotor
pytest tests/test_extract_article_contents.py -v
# Executar os testes do Extrator do Google News
pytest tests/test_extract_google_news.py -v
# Validação e correção automática de formatação com Ruff