Files
TextNLPClassifierApp/docs/prd_convert_json_markdown.md
T

22 KiB

PRD — Conversão de artigo JSON para Markdown

1. Visão geral

O TextNLPClassifierApp já possui scripts para extrair artigos com Trafilatura, Newspaper4k e Readability e para selecionar deterministicamente o melhor extrator de conteúdo.

Esta feature adicionará a etapa seguinte do pipeline: receber o JSON de um único artigo, já contendo selected_extractor, selecionar deterministicamente os metadados disponíveis e gerar um arquivo Markdown com o conteúdo do extrator escolhido.

O arquivo river_plate_extracted_selected(2).json foi usado como referência de estrutura. Embora esse arquivo contenha uma coleção em articles, a entrada operacional desta feature será somente um objeto individual dessa coleção.

2. Problema

Cada artigo possui três resultados de extração com campos, formatos e níveis de preenchimento diferentes. O selected_extractor define qual corpo tem o maior peso e deve ser utilizado, mas metadados úteis podem estar ausentes nesse extrator e disponíveis em outro.

É necessário produzir um Markdown único e previsível sem escolher novamente o melhor conteúdo, sem usar LLM e sem depender de interpretação manual.

3. Objetivo

Criar um CLI Python que:

  1. receba um arquivo JSON contendo exatamente um artigo;
  2. valide os campos obrigatórios;
  3. use exclusivamente o selected_extractor para obter o corpo do artigo;
  4. selecione título, URL original e metadados opcionais por regras determinísticas;
  5. converta o corpo HTML para Markdown quando necessário;
  6. grave um arquivo .md legível, consistente e pronto para as etapas posteriores do pipeline.

4. História do usuário

Como operador do pipeline de conteúdo, quero converter o JSON selecionado de um artigo em um arquivo Markdown para que o conteúdo e os melhores metadados disponíveis possam ser consumidos pelas etapas seguintes do sistema.

5. Escopo

5.1 Incluído

  • CLI Python.
  • Leitura de um arquivo JSON com um único artigo.
  • Suporte a trafilatura, newspaper4k e readability como valores de selected_extractor.
  • Seleção determinística de metadados.
  • Conversão de HTML para Markdown.
  • Uso direto do Markdown já gerado pela Trafilatura quando disponível.
  • Geração de um único arquivo .md por execução.
  • Validação de entrada, saída e erros.
  • Gravação atômica do arquivo de saída.
  • Testes unitários, testes do CLI e arquivos de resultado esperado.

5.2 Fora do escopo

  • Receber o objeto raiz com o array articles.
  • Processar vários artigos em uma execução.
  • Executar novamente Trafilatura, Newspaper4k ou Readability.
  • Calcular ou alterar selected_extractor.
  • Comparar, combinar ou complementar o corpo com conteúdo de outro extrator.
  • Usar LLM, embeddings ou qualquer seleção probabilística.
  • Fazer novas requisições HTTP.
  • Baixar ou armazenar imagens.
  • Limpar semanticamente publicidade, recomendações, overlays ou outros blocos editoriais presentes no corpo selecionado.
  • Criar API, banco de dados, fila, interface gráfica ou integração externa.
  • Alterar o JSON recebido.

6. Contrato de entrada

6.1 Formato

A entrada será um arquivo JSON UTF-8 cujo objeto raiz representa um único item do array articles observado no arquivo de referência.

Campos esperados no objeto:

Campo Tipo esperado Obrigatoriedade Uso
selected_extractor string Obrigatório Define a única fonte permitida para o corpo.
input_meta object Opcional Fornece principalmente URL original, título e data de fallback.
crawled_url string Opcional URL de fallback.
page_title string Opcional Título de fallback.
trafilatura object Condicional Obrigatório quando selecionado; opcional nos demais casos.
newspaper4k object Condicional Obrigatório quando selecionado; opcional nos demais casos.
readability object Condicional Obrigatório quando selecionado; opcional nos demais casos.

6.2 Valores aceitos para selected_extractor

  • trafilatura
  • newspaper4k
  • readability

Qualquer outro valor deve invalidar a entrada.

6.3 Campos obrigatórios após a resolução

O processamento somente será bem-sucedido se for possível resolver:

  • título não vazio;
  • URL original absoluta com protocolo http ou https;
  • corpo não vazio pertencente ao selected_extractor.

Os demais campos são opcionais e nunca devem impedir a geração do Markdown.

7. Contrato de saída

7.1 Arquivo

  • Formato: Markdown UTF-8.
  • Quantidade: um arquivo por execução.
  • Nome padrão: mesmo nome-base do JSON de entrada, substituindo .json por .md.
  • Caminho alternativo: informado por -o ou --output.
  • Escrita: arquivo temporário seguido de substituição atômica do destino.

7.2 Estrutura do Markdown

O Markdown deve seguir esta ordem:

# Título do artigo

Subtítulo ou descrição, quando disponível.

**Autor:** Nome do autor
**Publicado em:** 2026-08-20T00:36:33-03:00
**Site:** Nome do site
**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.jpg)

---

Conteúdo do artigo em Markdown.

Regras de apresentação:

  • título, URL original e corpo sempre devem aparecer;
  • cada linha opcional deve ser completamente omitida quando não houver valor válido;
  • nenhum placeholder como null, None, N/A, unknown ou [no-author] deve aparecer;
  • o subtítulo deve ser omitido quando for igual ao título após normalização;
  • a imagem principal deve ser omitida quando não possuir URL absoluta http ou https;
  • o nome do selected_extractor não deve ser exibido no documento;
  • os metadados técnicos internos do JSON não devem ser exibidos.

8. Regras funcionais

RF-001 — Receber um único artigo

O CLI deve aceitar somente um objeto individual de artigo. Um objeto contendo articles deve ser rejeitado, pois o processamento em lote não pertence a esta feature.

RF-002 — Respeitar o extrator selecionado

O corpo deve vir exclusivamente do extrator indicado em selected_extractor. A ausência de corpo utilizável nesse extrator deve encerrar o processamento com erro. O sistema não pode trocar silenciosamente para outro extrator.

RF-003 — Resolver o corpo dentro do extrator selecionado

selected_extractor Fonte principal Fallback do mesmo extrator Tratamento
trafilatura trafilatura.markdown trafilatura.text Usar o Markdown diretamente; o texto puro já é Markdown válido.
newspaper4k newspaper4k.article_html newspaper4k.text Converter o HTML; usar texto puro somente quando o HTML estiver vazio.
readability readability.cleaned_html readability.cleaned_text Converter o HTML; usar texto puro somente quando o HTML estiver vazio.

O fallback ocorre somente entre representações do mesmo extrator selecionado.

RF-004 — Selecionar metadados deterministicamente

Para cada campo, o sistema deve:

  1. percorrer as fontes na ordem definida neste PRD;
  2. normalizar e validar cada candidato;
  3. selecionar o primeiro candidato válido;
  4. não consultar as fontes restantes após a seleção;
  5. omitir o campo se nenhum candidato opcional for válido.

A mesma entrada deve sempre gerar a mesma seleção e o mesmo arquivo.

RF-005 — Priorizar metadados por campo

SELECIONADO representa o campo equivalente dentro do objeto indicado por selected_extractor. Quando o extrator não possuir o campo, essa posição é ignorada.

Campo de saída Ordem de prioridade
Título SELECIONADO.title → input_meta.titulo → page_title → newspaper4k.title → trafilatura.title → readability.title
URL original input_meta.url → crawled_url → URL canônica do selecionado → trafilatura.canonical_url → newspaper4k.canonical_link
Subtítulo/descrição descrição do selecionado → trafilatura.description → newspaper4k.meta_description → input_meta.subtitulo
Autores autor(es) do selecionado → newspaper4k.authors → trafilatura.author → readability.author
Data de publicação data do selecionado → newspaper4k.publish_date → trafilatura.date → input_meta.quando_publicado
Site site do selecionado → trafilatura.sitename → newspaper4k.meta_site_name → trafilatura.hostname → hostname da URL original
Categorias categorias do selecionado → trafilatura.categories
Tags tags do selecionado → trafilatura.tags → newspaper4k.tags → newspaper4k.meta_keywords
Palavras-chave newspaper4k.keywords → newspaper4k.meta_keywords
Idioma idioma do selecionado → trafilatura.language → newspaper4k.meta_lang
Imagem principal imagem do selecionado → newspaper4k.top_image → trafilatura.image

Mapeamento dos campos equivalentes do extrator selecionado:

Informação Trafilatura Newspaper4k Readability
Título title title title
Descrição description meta_description Não disponível
Autores author authors author
Data date publish_date Não disponível
Site sitename meta_site_name Não disponível
Categorias categories Não disponível Não disponível
Tags tags tags Não disponível
Idioma language meta_lang Não disponível
Imagem principal image top_image Não disponível
URL canônica canonical_url canonical_link Não disponível

RF-006 — Normalizar valores escalares

Antes da validação, toda string candidata deve:

  • ter entidades HTML decodificadas;
  • remover espaços no início e no fim;
  • colapsar sequências internas de espaços em um único espaço;
  • ser considerada ausente quando vazia ou quando corresponder, sem diferença entre maiúsculas e minúsculas, a um placeholder conhecido: null, none, n/a, unknown, [no-author] ou no-author.

RF-007 — Normalizar listas

Autores, categorias, tags e palavras-chave podem chegar como lista ou string. O sistema deve:

  • aceitar lista de strings;
  • aceitar string única;
  • separar strings com múltiplos valores apenas por ponto e vírgula;
  • normalizar cada item conforme RF-006;
  • descartar autor iniciado por http://, https:// ou www.;
  • eliminar duplicatas sem diferenciar maiúsculas de minúsculas, preservando a primeira grafia e a ordem original;
  • considerar a fonte inválida quando nenhum item válido restar;
  • usar somente a primeira fonte da tabela de prioridade que resultar em lista válida, sem unir listas de fontes diferentes.

RF-008 — Normalizar datas

  • Aceitar datas ISO 8601 e RFC 2822 observadas na entrada de referência.
  • Emitir ISO 8601.
  • Preservar o fuso horário informado.
  • Emitir somente YYYY-MM-DD quando a fonte fornecer apenas a data.
  • Considerar inválida uma data que não possa ser interpretada e continuar para a próxima fonte de prioridade.

RF-009 — Validar URLs

  • Aceitar somente URLs absolutas com protocolo http ou https.
  • Não fazer requisições para validar existência ou disponibilidade.
  • Não aceitar data:, javascript:, caminhos relativos ou strings sem hostname.

RF-010 — Converter HTML para Markdown

A conversão deve usar a biblioteca Python markdownify, configurada para produzir títulos no padrão ATX (#, ##, ###).

Devem ser preservados, quando presentes no HTML selecionado:

  • parágrafos;
  • títulos e subtítulos estruturais;
  • listas ordenadas e não ordenadas;
  • negrito e itálico;
  • links;
  • citações;
  • blocos de código;
  • tabelas suportadas pela biblioteca;
  • imagens válidas do próprio corpo.

A escolha de markdownify é intencional: a necessidade é exclusivamente converter HTML para Markdown. O Microsoft MarkItDown suporta HTML, mas atende vários outros formatos e acrescentaria uma abstração mais ampla do que a requerida por esta feature.

RF-011 — Tratar imagens do corpo

  • Preservar imagens convertidas do corpo somente quando o destino for uma URL absoluta http ou https.
  • Remover imagens com URL vazia, relativa ou data:.
  • Eliminar repetições exatas da mesma URL de imagem, preservando a primeira ocorrência.
  • Não adicionar ao corpo a coleção newspaper4k.images.
  • Não baixar, redimensionar ou validar remotamente imagens.

RF-012 — Evitar título duplicado

Após a conversão do corpo, o sistema deve remover o primeiro título H1 do corpo somente quando ele for igual ao título resolvido após decodificação de HTML, normalização de espaços e comparação sem diferença entre maiúsculas e minúsculas.

Outros títulos do conteúdo devem ser preservados.

RF-013 — Normalizar o Markdown final

O arquivo final deve:

  • usar quebra de linha LF;
  • terminar com exatamente uma quebra de linha;
  • eliminar espaços no final das linhas;
  • limitar sequências de linhas vazias a no máximo duas;
  • não conter tags HTML remanescentes geradas apenas pela estrutura do documento;
  • preservar o texto, a pontuação e os caracteres Unicode do conteúdo selecionado.

RF-014 — Não gerar saída parcial

Se ocorrer qualquer erro antes da conclusão, o arquivo de destino existente deve permanecer intacto e nenhum arquivo temporário deve permanecer no diretório de saída.

9. Interface CLI

9.1 Script

scripts/convert_article_to_markdown.py

9.2 Argumentos

Parâmetro Tipo Obrigatoriedade Padrão Descrição
-i, --input caminho Obrigatório — JSON contendo um único artigo.
-o, --output caminho Opcional <input_stem>.md Arquivo Markdown de destino.

Não devem ser adicionadas flags sem requisito funcional neste PRD.

9.3 Exemplos

python scripts/convert_article_to_markdown.py -i out/article_001.json

Resultado: out/article_001.md.

python scripts/convert_article_to_markdown.py \
  -i out/article_001.json \
  -o out/markdown/article_001.md

9.4 Saída do processo

  • Código 0: arquivo gerado com sucesso.
  • Código 2: argumentos inválidos, conforme comportamento do argparse.
  • Código 1: erro de leitura, validação, conversão ou gravação.
  • Mensagens de erro e confirmação devem ir para stderr.
  • O conteúdo Markdown não deve ser impresso no terminal quando houver arquivo de saída.

10. Tratamento de erros

O processamento deve falhar de forma clara nos seguintes casos:

Situação Comportamento esperado
Arquivo não encontrado ou ilegível Encerrar com código 1 e informar o caminho.
JSON inválido Encerrar com código 1 e informar que a entrada não é JSON válido.
Raiz diferente de objeto Encerrar com código 1.
Entrada contém articles Encerrar com código 1 e informar que o CLI aceita um único artigo.
selected_extractor ausente ou desconhecido Encerrar com código 1.
Objeto do extrator selecionado ausente Encerrar com código 1.
Corpo do extrator selecionado vazio Encerrar com código 1; não usar outro extrator.
Título não resolvido Encerrar com código 1.
URL original não resolvida ou inválida Encerrar com código 1.
Metadado opcional inválido Ignorar o candidato e tentar o próximo; omitir se todos falharem.
Falha na conversão Encerrar com código 1.
Falha na gravação Encerrar com código 1 sem alterar o destino anterior.

As mensagens não devem imprimir o conteúdo integral do artigo.

11. Requisitos não funcionais

RNF-001 — Determinismo

A mesma entrada e a mesma versão das dependências devem produzir exatamente o mesmo arquivo Markdown.

RNF-002 — Compatibilidade

A feature deve manter as versões de Python declaradas como suportadas pelo projeto e funcionar nos sistemas operacionais já suportados pelo repositório.

RNF-003 — Execução local

O processamento deve ocorrer inteiramente em memória local, sem rede, browser, LLM ou serviço externo.

RNF-004 — Integridade

A entrada não deve ser modificada. A gravação de saída deve ser atômica.

RNF-005 — Manutenibilidade

As regras de resolução de campos e as regras de conversão devem ser isoladas em funções testáveis, sem duplicação entre o CLI e o uso interno.

RNF-006 — Qualidade

O código deve atender aos gates já adotados pelo projeto: Ruff, Mypy, Pytest e SonarQube.

12. Critérios de aceite

CA-001 — Trafilatura selecionada

Dado um artigo com selected_extractor igual a trafilatura e trafilatura.markdown preenchido
Quando o CLI for executado
Então o corpo do arquivo deve vir de trafilatura.markdown e nenhum corpo dos demais extratores deve ser incorporado.

CA-002 — Newspaper4k selecionado

Dado um artigo com selected_extractor igual a newspaper4k e newspaper4k.article_html preenchido
Quando o CLI for executado
Então esse HTML deve ser convertido para Markdown preservando sua estrutura editorial.

CA-003 — Readability selecionado

Dado um artigo com selected_extractor igual a readability e readability.cleaned_html preenchido
Quando o CLI for executado
Então esse HTML deve ser convertido para Markdown preservando sua estrutura editorial.

CA-004 — Fallback dentro do extrator

Dado um extrator selecionado cujo campo estruturado esteja vazio, mas cujo campo de texto puro esteja preenchido
Quando o CLI for executado
Então o texto puro do mesmo extrator deve ser usado.

CA-005 — Proibição de fallback de corpo entre extratores

Dado um extrator selecionado sem HTML, Markdown ou texto utilizável e outro extrator com conteúdo
Quando o CLI for executado
Então o processamento deve falhar sem utilizar o outro extrator.

CA-006 — Metadado vindo de outro extrator

Dado um artigo cujo extrator selecionado não possua autor e outro extrator possua autor válido
Quando o CLI for executado
Então o primeiro autor válido conforme a prioridade deve aparecer no Markdown.

CA-007 — Obrigatórios presentes

Dado um artigo válido
Quando o Markdown for gerado
Então ele deve conter título, URL original e corpo não vazio.

CA-008 — Opcionais ausentes

Dado um artigo sem metadados opcionais válidos
Quando o Markdown for gerado
Então nenhuma linha vazia de metadado ou placeholder deve ser exibida.

CA-009 — Imagens inválidas

Dado um corpo com imagens data:, vazias ou relativas
Quando o conteúdo for convertido
Então essas imagens devem ser removidas do Markdown.

CA-010 — Título repetido no corpo

Dado um corpo que começa com H1 igual ao título resolvido
Quando o Markdown for montado
Então deve existir somente um H1 com esse título no arquivo final.

CA-011 — Entrada em lote rejeitada

Dado o arquivo completo de referência contendo articles
Quando ele for passado diretamente ao CLI
Então o processamento deve falhar informando que a entrada esperada é um único artigo.

CA-012 — Determinismo

Dado o mesmo JSON processado duas vezes com as mesmas dependências
Quando os arquivos forem comparados byte a byte
Então eles devem ser idênticos.

CA-013 — Gravação segura

Dado um arquivo de destino preexistente e uma falha durante o processamento
Quando o CLI encerrar
Então o arquivo preexistente deve continuar inalterado.

13. Estratégia de testes

13.1 Testes unitários

  • resolução de cada campo conforme a ordem de prioridade;
  • normalização de strings e placeholders;
  • normalização, deduplicação e seleção de listas;
  • parsing e padronização de datas ISO 8601 e RFC 2822;
  • validação de URLs;
  • seleção do corpo para cada extrator;
  • fallback de representação dentro do mesmo extrator;
  • bloqueio do fallback de corpo para outro extrator;
  • conversão dos principais elementos HTML;
  • remoção de imagens inválidas e duplicadas;
  • remoção somente do H1 inicial duplicado;
  • normalização final de espaços e quebras de linha.

13.2 Testes de integração do CLI

  • geração com caminho padrão;
  • geração com --output;
  • códigos de saída 0, 1 e 2;
  • mensagens em stderr;
  • rejeição do JSON com coleção articles;
  • preservação do destino em caso de falha;
  • codificação UTF-8 com caracteres acentuados.

13.3 Casos de resultado esperado

Devem existir pelo menos três fixtures válidas, uma para cada valor de selected_extractor, acompanhadas dos respectivos arquivos Markdown esperados. A comparação deve ser exata.

Também devem existir fixtures inválidas cobrindo:

  • JSON corrompido;
  • selected_extractor ausente;
  • extrator desconhecido;
  • corpo selecionado vazio;
  • título ausente em todas as fontes;
  • URL ausente ou inválida em todas as fontes;
  • objeto raiz contendo articles.

14. Definition of Done

A feature será considerada concluída quando:

  • o script scripts/convert_article_to_markdown.py estiver implementado;
  • markdownify estiver declarada nas dependências do projeto;
  • todos os requisitos funcionais e critérios de aceite estiverem cobertos;
  • os testes unitários e de integração estiverem passando;
  • as três fixtures de extratores produzirem exatamente os Markdown esperados;
  • Ruff não apontar erros;
  • Mypy não apontar erros;
  • o conjunto completo de testes do projeto permanecer aprovado;
  • o Quality Gate do SonarQube estiver aprovado;
  • o README documentar a nova etapa, os argumentos e exemplos do CLI;
  • nenhuma funcionalidade fora do escopo tiver sido adicionada.

15. Dependência técnica escolhida

  • Biblioteca: markdownify
  • Finalidade: conversão direta de HTML para Markdown em Python.
  • Alternativa avaliada: Microsoft MarkItDown.
  • Decisão: não usar MarkItDown nesta feature porque seu escopo de conversão multiformato excede a necessidade de HTML para Markdown.