Files
pastor-online/README.md
T
omp 2043d5ddcf 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)
2026-11-02 11:16:07 -03:00

8.6 KiB

Pastor Online

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.


Como funciona

./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 corpus. As citações são conferidas no servidor contra as fontes consultadas: uma 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.

Corpus estruturado. O texto vem de XML (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

Detalhes e o SHA-256 do arquivo em 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.


Requisitos

  • Docker com Compose v2 (ou Podman com podman-compose)
  • ~2 GB de RAM livres para o backend
  • ~1,5 GB de disco para o modelo de embeddings e o banco vetorial

Início rápido

1. Configure o provedor de LLM

cp .env.example .env

Edite o .env. Quatro caminhos possíveis:

Provedor Configuração Observação
Ollama (local) LLM_PROVIDER=ollama Subir com --profile local; o modelo é baixado na primeira execução
OpenAI LLM_PROVIDER=openai + OPENAI_API_KEY
Groq LLM_PROVIDER=groq + GROQ_API_KEY
Anthropic LLM_PROVIDER=anthropic + ANTHROPIC_API_KEY

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

# Com Ollama local:
docker compose --profile local up --build -d

# Sem Ollama (usando OpenAI, Groq ou Anthropic):
docker compose up --build -d

Acesse http://localhost:8080.

3. Acompanhe a indexação

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.

docker compose logs -f backend

Comandos úteis

# Reiniciar só o frontend (após mudanças no React)
docker compose up -d --build frontend

# Forçar a reindexação do corpus
curl -X POST http://localhost:8080/api/reindex

# Ver o estado da indexação
curl http://localhost:8080/api/status

# Rodar os testes do backend
docker compose exec backend pytest tests/ -v

# Rodar os testes do frontend
docker compose exec frontend npm test

API

Rota Método Descrição
/api/health GET Saúde do serviço
/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

O chat responde em Server-Sent Events:

event: sources
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..."}

event: done
data: {"citations":["Gênesis 1:1"]}

O frontend consome o stream com fetch + ReadableStream, porque EventSource não aceita POST.


Configuração

Todas as variáveis ficam no .env. As principais:

Variável Padrão Efeito
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; 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

Solução de problemas

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 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 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 indicado em OLLAMA_MODEL na primeira execução. Para outro modelo: OLLAMA_MODEL=llama3.2:3b docker compose --profile local up -d.

Porta 8080 já em uso. FRONTEND_PORT=8090 docker compose up -d. Atualize também CORS_ORIGINS.


Estrutura do projeto

.
├── docker-compose.yml
├── .env.example
├── 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
│   │   ├── corpus.py        # USFX → versículos + nomes canônicos dos livros
│   │   ├── embedding.py     # sentence-transformers
│   │   ├── 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/
├── frontend/
│   ├── Dockerfile
│   ├── nginx.conf
│   └── src/
│       ├── App.tsx
│       ├── api.ts
│       ├── lib/sse.ts
│       └── components/
└── docs/superpowers/        # design e plano de implementação

Segurança

  • As chaves de API vivem apenas no backend, via variáveis de ambiente. Nunca chegam ao bundle do navegador.
  • 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 últimas 10 trocas.
  • O endpoint de reindexação rejeita chamadas concorrentes (409) e aceita um token opcional (REINDEX_TOKEN).

A aplicação é local, sem autenticação. Não a exponha à internet sem colocar um proxy com autenticação à frente.