Files
TextNLPClassifierApp/README.md
T

546 lines
28 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)
- [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}")
```
---
## 📁 Estrutura do Projeto
```text
TextNLPClassifierApp/
├── classify.py # CLI principal do Classificador de Inerência
├── 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
│ ├── 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/ # Módulos centrais do classificador
│ ├── classifier.py # Orquestrador de classificação (Tier 1, 2, 3)
│ ├── models.py # Modelos de dados e esquemas (ECPSnapshot, Decision)
│ ├── preprocessor.py # Normalização de texto e detecção de idioma
│ └── adapters/ # Adaptadores opcionais de Embeddings e LLM
├── specs/ # Especificações e planos arquiteturais (Speckit)
│ ├── 001-multilingual-entity-classifier/
│ ├── 002-google-news-extractor/
│ ├── 003-article-content-extractor/
│ ├── 004-deterministic-content-selection/
│ └── 005-convert-json-markdown/ # Specs da conversão JSON para Markdown
├── tests/ # Suíte de testes automatizados
│ ├── test_classifier.py
│ ├── test_extract_google_news.py
│ ├── test_extract_article_contents.py
│ ├── test_select_article_extractor.py
│ ├── test_convert_article_to_markdown.py # Testes da conversão para Markdown
│ └── test_llm_fallback.py # Testes do Tier 3 LLM Fallback
├── 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 **196 testes automatizados** com 100% de aprovação cobrindo testes unitários, de regressão, de integração, Golden Fixtures exatas, testes de sensibilidade de mutação, testes de fallback para LLM (Tier 3) e testes End-to-End (E2E) via CLI subprocess:
```bash
# Executar toda a suíte de testes do projeto (196 testes)
pytest -v
# Executar os testes do Fallback para LLM (Tier 3)
pytest tests/test_llm_fallback.py -v
# Executar os testes de Conversão de Artigo para Markdown (67 testes)
pytest tests/test_convert_article_to_markdown.py -v
# Executar 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 com Ruff
ruff check .
# Verificação estática de tipos com Mypy
mypy src/ scripts/ tests/
```
---
## 📄 Licença
Este projeto está licenciado sob os termos da licença **MIT**. Consulte o arquivo `LICENSE` para mais detalhes.