Este repositório contém a implementação completa do Enter Extractor, uma solução de extração estruturada de PDFs desenvolvida para o desafio do Enter AI Fellowship. O sistema foi projetado para equilibrar acurácia (80%+), baixo custo por requisição e latência inferior a 10 s, mantendo um processo de aprendizagem incremental por documento.
O Enter Extractor combina heurística geométrica, validação algorítmica e aprendizado iterativo leve, atingindo o equilíbrio ideal entre custo, velocidade e precisão.
A analogia final é simples:
Cada campo começa como um “palpite” dentro de uma pirâmide de confiança. A cada documento, ele desce um degrau ou fica onde está, até se estabilizar como uma regra fixa. Quando todos os campos de um
labelamadurecem, o sistema se torna praticamente determinístico, além de dispor de caches modulares que permitem responder a combinações de chaves maduras com custo marginal zero, mesmo em configurações inéditas.
Extrair informações estruturadas de documentos PDF (com OCR embutido) a partir de um
label, umschemaparcial e o próprio arquivo, retornando JSONs consistentes, validados e cada vez mais rápidos à medida que a sessão evolui.
A arquitetura possui diferentes tipos de cache para operar sem reprocessar PDFs idênticos nem fazer chamadas redundantes ao LLM, além de aprender progressivamente a partir dos resultados anteriores — um processo que chamamos de maturação de receitas.
O pipeline é composto por três camadas principais:
- Pré-processamento e OCR:
Conversão do PDF em linhas e caixas delimitadoras (
bboxes) com opdfminer. - Extração determinística: Identificação de padrões (regex), alinhamentos geométricos e ancoragens contextuais.
- Backfill via LLM:
Chamadas únicas ao
gpt-5-minipara campos ausentes, usadas como ground truth para treinar e ajustar a confiança do sistema.
Todo o fluxo é cacheado e autoajustável em tempo de execução.
A ideia central é ensinar o sistema a se autoexplicar: cada campo é tratado como uma hipótese sobre o tipo de dado que ele representa. A maturação dessas hipóteses segue uma hierarquia de especificidade.
A inferência de tipo é o primeiro passo sempre que um novo campo (key) aparece em um determinado label.
Pode-se imaginar uma pirâmide de confiança, onde cada nível representa o quão específico e confiável é a crença que o sistema possui no contexto de determinado valor:
Quando o campo é verificável por formato (CPF, CNPJ, Data, CEP, etc.), ele é considerado um tipo de identidade única.
O sistema usa o mapa de sinônimos (enter_extractor/configs/synonyms_map.json) para identificar se o nome da chave sugere algum padrão validável e aplica os validadores formais (enter_extractor/regex/regex_validator_rules.txt. A depender da chave, checa-se apenas o formato. Para valores com checksum há essa validação mais rígida: CPF, CNPJ, PIS, Boleto, etc).
Critérios:
-
Apenas um match por documento (unicidade);
-
Busca em duas fases:
- BBox-clean: bloco isolado contendo apenas o padrão;
- Text-guarded: valor cercado por delimitadores (início/fim de linha ou palavra-chave);
-
Execução do validador de formato (checksum, data, moeda, UF etc.);
-
Se passar em todas as etapas, o campo é considerado regexable e seu padrão é armazenado como receita.
Caso o campo não seja verificável por regex, procuramos uma âncora textual próxima (por exemplo, “Nome:” à esquerda de “João da Silva”). Essa relação key → value é espacial, capturada por coordenadas (direção, distância e linha base).
Critérios:
-
O valor deve estar paralelo horizontal ou verticalmente ao rótulo (i.e. abaixo ou à direita do mesmo);
-
Não pode haver outro texto entre o rótulo e o valor;
-
A receita armazenada contém:
- BBox da âncora
- Direção (horizontal ou vertical)
- Regras de continuidade multiline
- Nas vezes seguintes que tratamos esse valor, procuramos pelo rótulo e percorremos o caminho reverso, esperando encontrar o valor.
Se nem regex nem âncora textual forem confiáveis, o sistema grava apenas a região geométrica do valor encontrado pelo LLM (x, y, w, h).
É como marcar no mapa: “da última vez que vi um CEP neste tipo de documento, ele estava aqui”. Um exemplo de valor Independent é o campo de nome na carteira da OAB fornecida como exemplo para este projeto.
Esses campos ainda são previsíveis — apenas não dependem de ancoragem textual.
Quando a confiança de um campo cai abaixo do limite de maturação (75%), ele é rebaixado a LLM-only. A partir desse ponto, ele sempre será extraído pelo modelo, sem tentativa de heurística.
Esse estágio representa o fim da pirâmide — um campo puramente semântico, não determinístico.
O leitor atento deve ter percebido que apenas assinalar um desses tipos a um valor na primeira vez que o encontramos é um tanto arriscado.
Cada (label, key) passa por um ciclo de maturação — um mecanismo de aprendizado baseado em feedback loops de reforço, iterados uma quantia fixa de vezes:
-
Predição: O sistema tenta extrair o valor com a receita atual (regex/aligned/independent).
-
Validação: Em paralelo, o LLM extrai os mesmos campos (para valores missing + maturing).
-
Comparação: Se o valor previsto coincide com o valor do LLM →
acertos++. -
Cálculo de confiança:
confiança = acertos / tentativas. -
Decisão:
- Se
confiança ≥ 0.8→ o campo matura (receita finalizada). - Se
confiança < 0.8após o mínimo de tentativas → o campo é rebaixado para o próximo tipo na hierarquia.
- Se
Todos os caches são em memória (sessão):
| Cache | Chave | Conteúdo | Capacidade |
|---|---|---|---|
| Recipe cache | (label, key) |
tipo, confiança, contadores | 10 000 |
| Doc cache | hash(PDF + schema) | JSON final | 500 |
| Parsed-page cache | hash(PDF) | linhas + bboxes | 250 |
Esses mecanismos permitem que o sistema reutilize o conhecimento local e responda rapidamente a PDFs repetidos ou semelhantes.
- Entrada:
(label, extraction_schema, pdf) - Normalização: limpa acentos, reduz a lower case e assimila sinônimos.
- OCR parsing: converte o PDF em blocos com texto e coordenadas.
- Busca de regexables: tenta validar padrões formais.
- LLM backfill: se necessário, executa uma única chamada com os campos ausentes.
- Fusão de resultados: combina matches regex + LLM.
- Resolução alinhada e independente: aplica regras geométricas Δy e regiões.
- Maturação: compara previsão × verdade do LLM, atualiza confiança.
- Demotion: campos com baixa confiança são rebaixados.
- Retorno: JSON final + métricas de custo/latência.
O sistema mantém contadores internos (em main.py) que acumulam:
- Total de requisições
- Custo médio em USD
- Latência média em segundos
Esses valores são retornados opcionalmente com --metrics.
enter_extractor/
├── main.py # CLI principal + maturação + métricas
├── core/
│ ├── cache.py # LRU caches de sessão
│ ├── schema.py # controle de maturidade por (label, key)
│ └── metrics.py # rolling averages
├── io/
│ ├── pdf_loader.py
│ └── ocr_parse.py # parser OCR (pdfminer)
├── regex/
│ ├── patterns.py # regexes canônicos
│ ├── detector.py # busca de padrões em texto
│ ├── validators.py # validações formais (CPF, CNPJ, etc.)
│ └── synonyms_map.json # sinônimos e expressões regulares
├── aligned/
│ └── resolve.py # resolução key↔value alinhada
├── independent/
│ └── resolve.py # captura por posição
└── pipeline/
├── llm.py # integração GPT-5-mini
└── context.py # preparação de schema e overrides
Na root do repo, rodar:
pip install -r requirements.txt
python -m enter_extractor.main path/to/requests.json path/to/pdf_or_folderpip install -r requirements.txt
uvicorn server:app --reload --workers 1Na pasta enter-ui/, rodar:
npm run dev{
"label": "carteira_oab",
"pdf_path": "oab_1.pdf",
"extraction_schema": {
"nome": "Nome do profissional",
"inscricao": "Número de inscrição",
"seccional": "Seccional do profissional"
}
}{
"pdf_path": "oab_1.pdf",
"label": "carteira_oab",
"extracted": {
"nome": "JOANA D'ARC",
"inscricao": "101943",
"seccional": "PR"
},
"metrics": {
"requests": 3,
"avg_cost_usd": 0.00053,
"avg_latency_s": 1.92
}
}
