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
+279
View File
@@ -0,0 +1,279 @@
# PRD — Seleção determinística da biblioteca de extração de conteúdo
**Versão:** 1.0
**Data:** 20/08/2026
**Status:** Pronto para implementação
## 1. Contexto
Cada artigo é processado pelas bibliotecas Trafilatura, Newspaper4k e Readability a partir do mesmo HTML. O resultado é consolidado em um arquivo JSON que contém, para cada artigo, as saídas das três bibliotecas.
É necessário escolher deterministicamente uma única biblioteca por artigo. A escolha deve ocorrer mesmo quando todos os resultados forem ruins. O processamento não pode retornar estado ambíguo nem deixar um artigo sem seleção.
## 2. Objetivo
Receber um arquivo JSON no formato do arquivo de referência, selecionar a melhor saída de extração disponível para cada artigo e gerar um novo arquivo JSON com os mesmos dados, acrescentando somente a chave `selected_extractor` em cada item de `articles`.
## 3. Escopo
### 3.1 Incluído
- Ler um arquivo JSON com uma coleção `articles`.
- Comparar as saídas de Trafilatura, Newspaper4k e Readability de cada artigo.
- Escolher obrigatoriamente uma das três bibliotecas.
- Adicionar `selected_extractor` em cada artigo.
- Gerar um novo arquivo JSON.
- Preservar os dados e a ordem dos artigos recebidos.
### 3.2 Fora do escopo
- Baixar ou renderizar páginas.
- Verificar se as bibliotecas receberam o mesmo HTML.
- Executar novamente as bibliotecas de extração.
- Limpar, recortar, combinar ou reescrever o conteúdo extraído.
- Gerar Markdown.
- Usar LLM, embeddings ou regras específicas por domínio.
- Alterar qualquer campo existente no JSON.
- Adicionar métricas, justificativas ou outras chaves ao arquivo de saída.
## 4. Premissas
- As três bibliotecas processaram exatamente o mesmo HTML.
- A entrada segue a estrutura do JSON de referência.
- A seleção é executada individualmente para cada artigo.
- O algoritmo deve sempre produzir uma escolha, inclusive em situações sem concordância entre as bibliotecas.
## 5. Entrada
### 5.1 Arquivo
- Formato: JSON válido.
- A raiz deve conter `articles` como uma lista.
- Cada item de `articles` representa um artigo.
### 5.2 Conteúdos comparados
| Biblioteca | Campo usado na comparação |
| ----------- | -------------------------- |
| Trafilatura | `trafilatura.text` |
| Newspaper4k | `newspaper4k.text` |
| Readability | `readability.cleaned_text` |
Os demais campos, incluindo título, descrição, resumo, palavras-chave, HTML estruturado e metadados, não participam da seleção.
### 5.3 Estado de um candidato
Para cada biblioteca, o candidato é classificado em um dos seguintes estados:
| Estado | Condição |
| ------------ | ---------------------------------------------------------------------------------- |
| Utilizável | Campo de conteúdo é uma string não vazia após normalização e `error` é nulo |
| Degradado | Campo de conteúdo é uma string não vazia após normalização, mas `error` não é nulo |
| Indisponível | Campo ausente, nulo, de tipo diferente de string ou vazio após normalização |
O campo de erro considerado é `trafilatura.error`, `newspaper4k.error` ou `readability.error`, conforme a biblioteca.
## 6. Saída
### 6.1 Arquivo
O arquivo de entrada nunca deve ser alterado. Deve ser criado um novo arquivo com o nome:
`<nome_original_sem_extensão>_selected.json`
Exemplo: `river_plate_extracted(2).json` gera `river_plate_extracted(2)_selected.json`.
### 6.2 Alteração permitida
Cada item de `articles` deve receber exatamente uma nova chave no mesmo nível de `trafilatura`, `newspaper4k` e `readability`:
`selected_extractor`
Valores permitidos:
- `trafilatura`
- `newspaper4k`
- `readability`
Não são permitidos `null`, string vazia, `ambiguous` ou qualquer outro valor.
### 6.3 Preservação da entrada
- Todas as chaves e valores existentes devem permanecer semanticamente idênticos.
- A ordem dos itens de `articles` deve ser preservada.
- Nenhum artigo pode ser adicionado ou removido.
- Espaçamento, indentação e ordem textual das chaves do JSON não fazem parte do contrato, pois o arquivo pode ser serializado novamente.
- Caso `selected_extractor` já exista, seu valor deve ser recalculado e substituído.
## 7. Algoritmo determinístico de seleção
### 7.1 Formar o conjunto ativo
Para cada artigo:
1. Identificar os candidatos utilizáveis.
2. Se existir pelo menos um utilizável, considerar somente os utilizáveis.
3. Se não existir utilizável, considerar os candidatos degradados.
4. Se não existir candidato utilizável nem degradado, selecionar `newspaper4k` pelo desempate final obrigatório.
5. Se o conjunto ativo possuir somente um candidato, selecioná-lo imediatamente.
### 7.2 Normalizar os conteúdos
A normalização serve apenas para comparação e não modifica o JSON de saída.
Para cada candidato ativo:
1. Decodificar entidades HTML.
2. Remover marcação HTML e Markdown, preservando o texto visível.
3. Em links, preservar o texto e remover o endereço.
4. Aplicar normalização Unicode NFKC.
5. Converter o texto para minúsculas.
6. Substituir toda sequência de espaços, tabulações ou quebras de linha por um único espaço.
7. Tokenizar mantendo letras e números Unicode.
8. Desconsiderar pontuação.
Nenhuma palavra ou trecho pode ser removido por interpretação semântica.
### 7.3 Gerar shingles
- Gerar a sequência ordenada de tokens de cada candidato.
- Formar o conjunto de todas as janelas consecutivas de cinco tokens.
- Quando o candidato possuir entre um e quatro tokens, usar a sequência completa como um único shingle.
- Candidato sem token é indisponível e não participa do conjunto ativo.
### 7.4 Construir o consenso
O consenso é o conjunto de shingles presentes em pelo menos dois candidatos ativos.
Para cada candidato ativo, calcular:
**Cobertura:**
`coverage = quantidade de shingles do consenso presentes no candidato / quantidade total de shingles do consenso`
**Suporte:**
`support = quantidade de shingles do candidato presentes no consenso / quantidade total de shingles do candidato`
**Pontuação:**
`score = 2 × coverage × support / (coverage + support)`
Quando `coverage + support` for zero, a pontuação será zero.
### 7.5 Selecionar quando existe consenso
1. Ordenar os candidatos por `score`, do maior para o menor.
2. Identificar o maior `score`.
3. Considerar empate técnico todo candidato cuja diferença para o maior `score` seja menor ou igual a `0,03`.
4. Se houver apenas um candidato no empate técnico, selecioná-lo.
5. Se houver empate técnico, selecionar o candidato com a menor quantidade de shingles.
6. Se a quantidade de shingles também empatar, aplicar a prioridade final:
1. `newspaper4k`
2. `readability`
3. `trafilatura`
A preferência pelo menor candidato ocorre somente no empate técnico. Nesse cenário, os candidatos possuem qualidade de concordância equivalente, e a decisão favorece menor conteúdo excedente.
### 7.6 Selecionar quando não existe consenso
Quando nenhum shingle aparece em pelo menos dois candidatos ativos:
- Com três candidatos ativos: selecionar o candidato com a quantidade mediana de shingles.
- Com dois candidatos ativos: selecionar o candidato com a maior quantidade de shingles.
- Com um candidato ativo: selecionar o único candidato.
- Em empate de quantidade: aplicar a prioridade `newspaper4k`, `readability`, `trafilatura`.
- Sem candidato ativo: selecionar `newspaper4k`.
Essas regras garantem uma escolha mesmo quando não existe concordância textual.
### 7.7 Gravar a escolha
Adicionar ou substituir `selected_extractor` no artigo com o identificador da biblioteca vencedora. Repetir o processo até que todos os itens de `articles` tenham sido processados.
## 8. Requisitos funcionais
| ID | Requisito |
| ------ | ------------------------------------------------------------------------------------------------------- |
| FR-001 | O sistema deve aceitar um arquivo JSON como entrada. |
| FR-002 | O sistema deve validar que a raiz é um objeto e que `articles` é uma lista. |
| FR-003 | O sistema deve processar todos os artigos, preservando sua ordem. |
| FR-004 | O sistema deve usar exclusivamente os três campos de conteúdo definidos no PRD para calcular a escolha. |
| FR-005 | O sistema deve executar a normalização e a comparação conforme o algoritmo deste PRD. |
| FR-006 | O sistema deve selecionar exatamente uma biblioteca por artigo. |
| FR-007 | O sistema nunca deve produzir resultado ambíguo. |
| FR-008 | O sistema deve adicionar somente `selected_extractor` em cada artigo. |
| FR-009 | O valor de `selected_extractor` deve pertencer ao catálogo fechado de valores permitidos. |
| FR-010 | O sistema deve preservar todas as chaves e valores recebidos. |
| FR-011 | O sistema deve gerar um novo arquivo e manter o arquivo original inalterado. |
| FR-012 | O sistema deve produzir a mesma seleção sempre que receber exatamente a mesma entrada. |
| FR-013 | O sistema deve recalcular `selected_extractor` quando a chave já existir. |
## 9. Tratamento de erros
| Situação | Comportamento obrigatório |
| ---------------------------------------- | ----------------------------------------------------- |
| JSON inválido | Encerrar o processamento e não gerar arquivo de saída |
| Raiz diferente de objeto | Encerrar o processamento e não gerar arquivo de saída |
| `articles` ausente ou diferente de lista | Encerrar o processamento e não gerar arquivo de saída |
| `articles` vazio | Gerar arquivo com lista vazia e sem outras alterações |
| Estrutura de uma biblioteca ausente | Tratar seu candidato como indisponível |
| Campo de conteúdo com tipo inválido | Tratar seu candidato como indisponível |
| Todas as bibliotecas indisponíveis | Selecionar `newspaper4k` |
| Falha ao gravar o arquivo | Não deixar arquivo de saída parcialmente gravado |
Um erro em um artigo não pode impedir a seleção dos demais artigos, desde que o JSON e a lista `articles` sejam válidos.
## 10. Requisitos não funcionais
| ID | Requisito |
| ------- | ----------------------------------------------------------------------------------------- |
| NFR-001 | O processamento deve ser totalmente determinístico. |
| NFR-002 | O processamento não deve realizar chamadas de rede. |
| NFR-003 | O processamento não deve depender de LLM, embeddings ou serviços externos. |
| NFR-004 | O processamento deve operar somente sobre os dados do arquivo recebido. |
| NFR-005 | A gravação do arquivo deve ser atômica: sucesso completo ou ausência do arquivo de saída. |
## 11. Critérios de aceite
1. Dado o JSON de referência com 20 artigos, o arquivo de saída contém os mesmos 20 artigos na mesma ordem.
2. Cada artigo contém exatamente um `selected_extractor` válido.
3. Nenhum artigo contém `selected_extractor` nulo, vazio ou ambíguo.
4. Todas as chaves e valores anteriores permanecem semanticamente idênticos.
5. Nenhuma chave adicional, além de `selected_extractor`, é criada.
6. O arquivo original permanece inalterado.
7. Duas execuções sobre o mesmo arquivo produzem os mesmos valores de `selected_extractor`.
8. A seleção usa somente `trafilatura.text`, `newspaper4k.text` e `readability.cleaned_text`.
9. Quando todas as saídas estiverem vazias ou indisponíveis, `selected_extractor` recebe `newspaper4k`.
10. Quando não houver consenso, o desempate segue exatamente as regras da seção 7.6.
11. Quando houver empate técnico, o desempate segue exatamente as regras da seção 7.5.
12. Um JSON inválido ou sem `articles` válido não produz arquivo parcial.
## 12. Casos obrigatórios de teste
| Caso | Condição | Resultado esperado |
| ------ | ---------------------------------------------------------------------------- | ------------------------------------------------- |
| CT-001 | Três candidatos com consenso e um vencedor claro | Selecionar o maior `score` |
| CT-002 | Dois ou mais candidatos dentro de `0,03` do maior `score` | Selecionar o de menor quantidade de shingles |
| CT-003 | Empate técnico e mesma quantidade de shingles | Aplicar prioridade final |
| CT-004 | Três candidatos sem consenso | Selecionar a quantidade mediana de shingles |
| CT-005 | Dois candidatos sem consenso | Selecionar a maior quantidade de shingles |
| CT-006 | Somente um candidato utilizável | Selecionar esse candidato |
| CT-007 | Nenhum utilizável, mas existe candidato degradado | Executar o algoritmo somente com os degradados |
| CT-008 | Todos os candidatos indisponíveis | Selecionar `newspaper4k` |
| CT-009 | Readability retorna apenas um fragmento pequeno enquanto os outros concordam | O fragmento perde por baixa cobertura do consenso |
| CT-010 | Um candidato contém o conteúdo comum e muito conteúdo excedente | O candidato perde suporte e reduz sua pontuação |
| CT-011 | Um candidato contém somente parte do conteúdo comum | O candidato perde cobertura e reduz sua pontuação |
| CT-012 | A entrada já contém `selected_extractor` | Recalcular e substituir somente essa chave |
| CT-013 | `articles` está vazio | Gerar saída válida com `articles` vazio |
| CT-014 | JSON inválido | Não gerar saída |
## 13. Definition of Done
- Todos os requisitos funcionais foram implementados.
- Todos os casos obrigatórios de teste foram automatizados e aprovados.
- O JSON de referência com 20 artigos é processado integralmente.
- A saída contém somente a inclusão de `selected_extractor` em cada artigo.
- O arquivo original permanece inalterado.
- Execuções repetidas sobre a mesma entrada produzem as mesmas escolhas.
- Não existe caminho de execução que produza `ambiguous`, `null` ou artigo sem seleção.