feat(converter): implement deterministic JSON to Markdown article converter (spec 005)
This commit is contained in:
@@ -35,6 +35,13 @@
|
||||
- [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)
|
||||
@@ -49,6 +56,7 @@ O **TextNLPClassifierApp** reúne um ecossistema completo de ferramentas de enge
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -353,47 +361,147 @@ 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)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
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
|
||||
├── 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
|
||||
├── 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)
|
||||
│ ├── __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/ # Specs da seleção determinística
|
||||
├── tests/ # Suíte de testes automatizados
|
||||
│ ├── 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 # 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
|
||||
│ ├── test_select_article_extractor.py
|
||||
│ └── test_convert_article_to_markdown.py # Testes da conversão para Markdown
|
||||
├── 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 **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:
|
||||
O repositório possui **187 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 e testes End-to-End (E2E) via CLI subprocess:
|
||||
|
||||
```bash
|
||||
# Executar toda a suíte de testes do projeto (120 testes)
|
||||
# Executar toda a suíte de testes do projeto (187 testes)
|
||||
pytest -v
|
||||
|
||||
# Executar especificamente os testes do Seletor Determinístico
|
||||
# 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
|
||||
@@ -402,12 +510,11 @@ 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
|
||||
ruff check --fix .
|
||||
ruff format .
|
||||
# Validação com Ruff
|
||||
ruff check .
|
||||
|
||||
# Verificação estática de tipos com Mypy
|
||||
mypy scripts/ src/
|
||||
mypy scripts/convert_article_to_markdown.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user