refactor: troca PDF+OCR por corpus USFX em domínio público
- corpus/: Almeida (João Ferreira de Almeida) em USFX, domínio público, 66 livros / 1189 capítulos / 31098 versículos, versionado com SHA-256 - app/corpus.py: parser USFX (texto no tail de <v/>), nomes canônicos dos livros, aliases (abreviaturas e numerais romanos), VerseChunk sem página - indexer: descobre *.xml em CORPUS_DIR, progresso em versículos, lote configurável, manifesto incremental por SHA-256 - remove extraction.py, parallel.py, bible_parser.py, Tesseract e PyMuPDF - campo page removido de Source/SearchResult/StatusResponse e do frontend - embedder: propriedade name; status reporta o embedder em uso, não o do .env - create_app constrói o embedder ANTES do store (o default HashEmbedder do VectorStore impedia o modelo configurado de ser carregado em produção) - fallback do modelo agora aparece em /api/status.errors - torch 2.6.0 + transformers 5.22.1 pinados (transformers 5.x exige torch>=2.6)
This commit is contained in:
1 parent
621679cee5
commit
2043d5ddcf
34 files changed
+34561
-1842
No files matched your search
@@ -1,8 +1,8 @@
|
||||
# Pastor Online
|
||||
|
||||
Sistema web RAG para consulta bíblica. Lê PDFs da Bíblia, indexa o conteúdo em um
|
||||
banco vetorial e responde em chat com um agente pastoral **ancorado exclusivamente nos
|
||||
versículos recuperados** — com as referências sempre à vista.
|
||||
Sistema web RAG para consulta bíblica. Indexa um corpus bíblico em um banco vetorial e
|
||||
responde em chat com um agente pastoral **ancorado exclusivamente nos versículos
|
||||
recuperados** — com as referências sempre à vista.
|
||||
|
||||
Containerizado com Docker Compose: frontend React, backend FastAPI, ChromaDB e
|
||||
(opcionalmente) Ollama local.
|
||||
@@ -12,11 +12,9 @@ Containerizado com Docker Compose: frontend React, backend FastAPI, ChromaDB e
|
||||
## Como funciona
|
||||
|
||||
```
|
||||
./PDF/*.pdf ──▶ PyMuPDF + OCR (Tesseract) ──▶ parser bíblico ──▶ embeddings
|
||||
│
|
||||
navegador ◀── SSE ── FastAPI ◀── busca + validação
|
||||
│
|
||||
ChromaDB
|
||||
./corpus/*.usfx.xml ──▶ parser USFX ──▶ embeddings ──▶ ChromaDB
|
||||
│
|
||||
navegador ◀── SSE ── FastAPI ◀── busca + validação de citações
|
||||
```
|
||||
|
||||
**Fidelidade ao texto.** A resposta só é gerada quando há evidência recuperada do
|
||||
@@ -24,10 +22,30 @@ corpus. As citações são conferidas no servidor contra as fontes consultadas:
|
||||
referência que o modelo inventou é removida **antes** de chegar à tela. Sem evidência,
|
||||
o sistema responde com uma mensagem fixa e não chama o LLM.
|
||||
|
||||
**Extração.** Se a camada textual do PDF é utilizável, ela é usada (rápido). Se o PDF
|
||||
usa fontes sem mapeamento Unicode — caso comum em PDFs de Bíblia digitalizados — o
|
||||
sistema detecta a corrupção e aplica OCR na página. O resultado do OCR é cacheado por
|
||||
página: reindexações não repetem o trabalho.
|
||||
**Corpus estruturado.** O texto vem de XML ([USFX](https://ebible.org/usfx/)), não de
|
||||
PDF: cada versículo já chega com livro, capítulo e número próprios. Não há OCR, nem
|
||||
reconstrução de capítulos a partir de marcadores de página, nem ruído de
|
||||
digitalização — a referência exibida é exatamente a coordenada de onde o texto foi
|
||||
lido.
|
||||
|
||||
---
|
||||
|
||||
## Corpus
|
||||
|
||||
Vem versionado em **`./corpus`**, pronto para uso:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Tradução | João Ferreira de Almeida |
|
||||
| Licença | **Domínio público** — sem atribuição nem restrição |
|
||||
| Conteúdo | 66 livros · 1.189 capítulos · 31.098 versículos |
|
||||
| Proveniência | [seven1m/open-bibles](https://github.com/seven1m/open-bibles) |
|
||||
|
||||
Detalhes e o SHA-256 do arquivo em [`corpus/README.md`](corpus/README.md).
|
||||
|
||||
Para usar outra tradução, coloque um arquivo USFX em `./corpus` e reinicie o backend
|
||||
(ou chame `POST /api/reindex`). **Verifique a licença antes**: NVI, NTLH, NAA, ACF,
|
||||
ARA, ARC e KJA pertencem às editoras e não podem ser redistribuídas.
|
||||
|
||||
---
|
||||
|
||||
@@ -41,21 +59,13 @@ página: reindexações não repetem o trabalho.
|
||||
|
||||
## Início rápido
|
||||
|
||||
### 1. Coloque os PDFs
|
||||
|
||||
Copie seus PDFs da Bíblia para a pasta **`./PDF`**. O diretório já contém o
|
||||
`biblia.pdf` desta edição da Almeida Revista e Corrigida.
|
||||
|
||||
> A pasta `./pdfs` (minúscula) também funciona: basta apontar `PDF_DIR` para ela no
|
||||
> `docker-compose.yml`. O Compose monta `./PDF` como somente-leitura em `/data/pdfs`.
|
||||
|
||||
### 2. Configure o provedor de LLM
|
||||
### 1. Configure o provedor de LLM
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edite o `.env`. Três caminhos possíveis:
|
||||
Edite o `.env`. Quatro caminhos possíveis:
|
||||
|
||||
| Provedor | Configuração | Observação |
|
||||
|---|---|---|
|
||||
@@ -64,7 +74,11 @@ Edite o `.env`. Três caminhos possíveis:
|
||||
| **Groq** | `LLM_PROVIDER=groq` + `GROQ_API_KEY` | |
|
||||
| **Anthropic** | `LLM_PROVIDER=anthropic` + `ANTHROPIC_API_KEY` | |
|
||||
|
||||
### 3. Suba o sistema
|
||||
Para testar sem nenhum provedor, use `LLM_PROVIDER=stub`: devolve resposta
|
||||
determinística a partir do contexto recuperado, útil para conferir a busca e as
|
||||
citações.
|
||||
|
||||
### 2. Suba o sistema
|
||||
|
||||
```bash
|
||||
# Com Ollama local:
|
||||
@@ -76,13 +90,12 @@ docker compose up --build -d
|
||||
|
||||
Acesse **http://localhost:8080**.
|
||||
|
||||
### 4. Acompanhe a indexação
|
||||
### 3. Acompanhe a indexação
|
||||
|
||||
A primeira indexação faz OCR de todas as páginas do PDF — são ~3.400 páginas, e o
|
||||
tempo típico fica entre 15 e 30 minutos em CPU. O painel lateral mostra o progresso
|
||||
(PDFs lidos e vetores indexados) e o chat já responde durante a indexação.
|
||||
|
||||
Para acompanhar os logs:
|
||||
A indexação começa sozinha no arranque. O trabalho é todo embedding de 31.098
|
||||
versículos em CPU — o painel lateral mostra o progresso por versículo e o chat já
|
||||
responde enquanto ela roda. Reiniciar o container não repete o trabalho: o manifesto
|
||||
guarda o SHA-256 de cada arquivo e só reprocessa o que mudou.
|
||||
|
||||
```bash
|
||||
docker compose logs -f backend
|
||||
@@ -96,7 +109,7 @@ docker compose logs -f backend
|
||||
# Reiniciar só o frontend (após mudanças no React)
|
||||
docker compose up -d --build frontend
|
||||
|
||||
# Forçar a reindexação dos PDFs
|
||||
# Forçar a reindexação do corpus
|
||||
curl -X POST http://localhost:8080/api/reindex
|
||||
|
||||
# Ver o estado da indexação
|
||||
@@ -116,7 +129,7 @@ docker compose exec frontend npm test
|
||||
| Rota | Método | Descrição |
|
||||
|---|---|---|
|
||||
| `/api/health` | GET | Saúde do serviço |
|
||||
| `/api/status` | GET | PDFs lidos, vetores indexados, erros |
|
||||
| `/api/status` | GET | Fontes lidas, versículos indexados, progresso, erros |
|
||||
| `/api/reindex` | POST | Força a reindexação (202). 409 se já em andamento |
|
||||
| `/api/chat` | POST | Chat em streaming (SSE) |
|
||||
| `/api/sources` | GET | Trechos consultados na última resposta |
|
||||
@@ -125,7 +138,7 @@ O chat responde em Server-Sent Events:
|
||||
|
||||
```
|
||||
event: sources
|
||||
data: {"sources":[{"reference":"Gênesis 1:1","text":"...","page":6}]}
|
||||
data: {"sources":[{"reference":"Gênesis 1:1","text":"...","book":"Gênesis","chapter":1,"verse":1}]}
|
||||
|
||||
event: token
|
||||
data: {"text":"No princípio criou Deus os céus e a terra..."}
|
||||
@@ -145,12 +158,12 @@ Todas as variáveis ficam no `.env`. As principais:
|
||||
|
||||
| Variável | Padrão | Efeito |
|
||||
|---|---|---|
|
||||
| `LLM_PROVIDER` | `ollama` | `ollama`, `openai`, `groq` ou `anthropic` |
|
||||
| `LLM_PROVIDER` | `ollama` | `ollama`, `openai`, `groq`, `anthropic` ou `stub` |
|
||||
| `EMBEDDING_MODEL` | `paraphrase-multilingual-MiniLM-L12-v2` | `all-MiniLM-L6-v2` é menor e mais rápido |
|
||||
| `TOP_K` | `6` | Quantos trechos são recuperados por pergunta |
|
||||
| `MIN_SIMILARITY` | `0.35` | Corte de relevância; acima disso, o sistema responde que o texto não aborda o assunto |
|
||||
| `OCR_DPI` | `200` | Resolução do OCR |
|
||||
| `OCR_LANGUAGE` | `por+eng` | Idiomas do Tesseract |
|
||||
| `MIN_SIMILARITY` | `0.35` | Corte de relevância; abaixo disso o sistema responde que o texto não aborda o assunto |
|
||||
| `INDEX_BATCH_SIZE` | `512` | Versículos gravados no banco por lote |
|
||||
| `CORPUS_DIR` | `/data/corpus` | Diretório dos arquivos USFX (montado de `./corpus`) |
|
||||
| `REINDEX_TOKEN` | vazio | Se preenchido, `/api/reindex` exige o header `X-Reindex-Token` |
|
||||
| `CORS_ORIGINS` | `http://localhost:8080` | Origens permitidas |
|
||||
| `AUTO_INDEX` | `true` | Indexa automaticamente na inicialização |
|
||||
@@ -159,22 +172,20 @@ Todas as variáveis ficam no `.env`. As principais:
|
||||
|
||||
## Solução de problemas
|
||||
|
||||
**A indexação não termina / fica em "Indexando".**
|
||||
O OCR é o gargalo. Verifique `docker compose logs backend`. Se o OCR estiver lento,
|
||||
aumente `OCR_DPI` não ajuda — reduza para 150 para acelerar, com perda de acurácia.
|
||||
Para retomar após uma interrupção, basta reiniciar o container: o cache de OCR é
|
||||
preservado no volume e só as páginas faltantes são reprocessadas.
|
||||
|
||||
**O chat responde que o texto "não aborda diretamente o assunto".**
|
||||
O índice pode estar vazio (`/api/status`) ou a pergunta pode estar longe do corpus.
|
||||
Reduza `MIN_SIMILARITY` para 0.25, ou increase `TOP_K` para 10. Verifique também se a
|
||||
pergunta cita uma referência explícita — nesse caso a busca é determinística e não
|
||||
depende do limiar.
|
||||
Reduza `MIN_SIMILARITY` para 0.25, ou aumente `TOP_K` para 10. Se a pergunta citar
|
||||
uma referência explícita, a busca é determinística e não depende do limiar.
|
||||
|
||||
**"Nenhum versículo pôde ser extraído".**
|
||||
O PDF pode não ser de uma Bíblia com capítulos e versículos numerados, ou o OCR
|
||||
pode ter falhado. Inspecione o cache de OCR em `/data/vector_db/ocr_cache/` para ver
|
||||
o texto que foi extraído de cada página.
|
||||
**"nenhum versículo pôde ser lido" em `/api/status`.**
|
||||
O arquivo em `./corpus` não é um USFX válido, ou não contém `<book>`. Confira com:
|
||||
`docker compose exec backend python -c "from app.corpus import parse_usfx; print(sum(1 for _ in parse_usfx('/data/corpus/NOME.xml')))"`.
|
||||
|
||||
**A reindexação não pega um arquivo novo.**
|
||||
Só `*.xml` é descoberto. Confira o nome e a permissão de leitura em `./corpus`.
|
||||
|
||||
**Quero reindexar do zero.**
|
||||
`docker compose down && docker volume rm biblia-online_vector_db && docker compose up -d`.
|
||||
|
||||
**Ollama não sobe.**
|
||||
`docker compose --profile local up -d`. O serviço `ollama-pull` baixa o modelo
|
||||
@@ -192,19 +203,18 @@ indicado em `OLLAMA_MODEL` na primeira execução. Para outro modelo:
|
||||
.
|
||||
├── docker-compose.yml
|
||||
├── .env.example
|
||||
├── PDF/ # PDFs da Bíblia (entrada, somente-leitura)
|
||||
├── corpus/ # texto bíblico indexado (USFX, domínio público)
|
||||
├── backend/
|
||||
│ ├── Dockerfile
|
||||
│ ├── requirements.txt
|
||||
│ ├── app/
|
||||
│ │ ├── config.py # settings via pydantic-settings
|
||||
│ │ ├── models.py # schemas da API
|
||||
│ │ ├── extraction.py # PDF → texto (PyMuPDF + OCR + cache)
|
||||
│ │ ├── bible_parser.py # livro / capítulo / versículo
|
||||
│ │ ├── corpus.py # USFX → versículos + nomes canônicos dos livros
|
||||
│ │ ├── embedding.py # sentence-transformers
|
||||
│ │ ├── vector_store.py # ChromaDB
|
||||
│ │ ├── indexer.py # manifesto incremental
|
||||
│ │ ├── llm.py # adaptadores Ollama/OpenAI/Groq/Anthropic
|
||||
│ │ ├── vector_store.py # ChromaDB (+ versão em memória para testes)
|
||||
│ │ ├── indexer.py # descoberta, manifesto incremental, progresso
|
||||
│ │ ├── llm.py # adaptadores Ollama/OpenAI/Groq/Anthropic/stub
|
||||
│ │ ├── chat.py # contexto, validação de citações
|
||||
│ │ └── main.py # rotas FastAPI
|
||||
│ └── tests/
|
||||
@@ -225,7 +235,7 @@ indicado em `OLLAMA_MODEL` na primeira execução. Para outro modelo:
|
||||
|
||||
- As chaves de API vivem **apenas** no backend, via variáveis de ambiente. Nunca
|
||||
chegam ao bundle do navegador.
|
||||
- Os PDFs são montados somente-leitura; a escrita fica restrita ao volume `/data`.
|
||||
- O corpus é montado somente-leitura; a escrita fica restrita ao volume `/data`.
|
||||
- O conteúdo indexado é entregue ao modelo como **dado delimitado**, nunca como
|
||||
instrução — o que neutraliza tentativas de prompt injection via corpus.
|
||||
- Mensagens acima de 4.000 caracteres são recusadas; o histórico é limitado às
|
||||
|
||||
Reference in new issue
Block a user