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
@@ -45,18 +45,18 @@ class FakeIndexer:
|
||||
self.release.wait(timeout=10)
|
||||
from app.indexer import IndexStatus
|
||||
|
||||
return IndexStatus(state="ready", pdfs_total=1, pdfs_indexed=1, chunks=42)
|
||||
return IndexStatus(state="ready", sources_total=1, sources_indexed=1, chunks=42)
|
||||
|
||||
def status(self):
|
||||
from app.indexer import IndexStatus
|
||||
|
||||
return IndexStatus(state="ready", pdfs_total=1, pdfs_indexed=1, chunks=42)
|
||||
return IndexStatus(state="ready", sources_total=1, sources_indexed=1, chunks=42)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings(tmp_path):
|
||||
return Settings(
|
||||
pdf_dir=tmp_path / "pdfs",
|
||||
corpus_dir=tmp_path / "corpus",
|
||||
vector_db_dir=tmp_path / "vector_db",
|
||||
embedding_model="hash-test",
|
||||
llm_provider="stub",
|
||||
@@ -156,7 +156,7 @@ def test_chat_rejects_message_above_the_limit(client):
|
||||
|
||||
|
||||
def test_chat_streams_sources_token_and_done(client):
|
||||
from app.bible_parser import VerseChunk
|
||||
from app.corpus import VerseChunk
|
||||
|
||||
embedder = HashEmbedder(dimensions=64)
|
||||
store = client.app.state.services.store
|
||||
@@ -164,8 +164,7 @@ def test_chat_streams_sources_token_and_done(client):
|
||||
book="Gênesis",
|
||||
chapter=1,
|
||||
verse=1,
|
||||
page=6,
|
||||
file="biblia.pdf",
|
||||
file="biblia.usfx.xml",
|
||||
text="No princípio criou Deus os céus e a terra.",
|
||||
)
|
||||
store.upsert([chunk], embedder.encode([chunk.text]))
|
||||
@@ -213,3 +212,69 @@ def test_sources_endpoint_returns_a_list(client):
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "sources" in response.json()
|
||||
|
||||
|
||||
def test_create_app_builds_the_configured_embedder(monkeypatch, tmp_path):
|
||||
"""Produção: o modelo configurado precisa ser construído de fato.
|
||||
|
||||
O `VectorStore` default para `HashEmbedder` quando nasce sem embedder. Se a
|
||||
aplicação criar o store primeiro e depois herdar o embedder dele, o modelo
|
||||
configurado nunca é carregado e a busca roda sem semântica nenhuma — com
|
||||
`/api/status` ainda anunciando o nome do modelo.
|
||||
"""
|
||||
import app.main as main_module
|
||||
|
||||
built: list[str] = []
|
||||
|
||||
class FakeSentenceTransformer:
|
||||
def __init__(self, model_name, batch_size=32, device="cpu", module=None):
|
||||
built.append(model_name)
|
||||
self.name = model_name
|
||||
self.dimensions = 8
|
||||
|
||||
def encode(self, texts):
|
||||
return [[0.0] * 8 for _ in texts]
|
||||
|
||||
monkeypatch.setattr(main_module, "SentenceTransformerEmbedder", FakeSentenceTransformer)
|
||||
|
||||
settings = Settings(
|
||||
corpus_dir=tmp_path / "corpus",
|
||||
vector_db_dir=tmp_path / "vector_db",
|
||||
embedding_model="modelo-configurado",
|
||||
llm_provider="stub",
|
||||
auto_index=False,
|
||||
)
|
||||
|
||||
app = main_module.create_app(settings=settings)
|
||||
services = app.state.services
|
||||
|
||||
assert built == ["modelo-configurado"]
|
||||
assert services.embedder.name == "modelo-configurado"
|
||||
assert services.store.embedder.name == "modelo-configurado"
|
||||
assert services.embedder_warning is None
|
||||
|
||||
|
||||
def test_embedder_fallback_is_surfaced_in_status(monkeypatch, tmp_path):
|
||||
"""Se o modelo não carrega, o status precisa dizer que a busca está sem semântica."""
|
||||
import app.main as main_module
|
||||
|
||||
def explode(*args, **kwargs):
|
||||
raise RuntimeError("PyTorch >= 2.6 is required but found 2.5.1")
|
||||
|
||||
monkeypatch.setattr(main_module, "SentenceTransformerEmbedder", explode)
|
||||
|
||||
settings = Settings(
|
||||
corpus_dir=tmp_path / "corpus",
|
||||
vector_db_dir=tmp_path / "vector_db",
|
||||
embedding_model="modelo-configurado",
|
||||
llm_provider="stub",
|
||||
auto_index=False,
|
||||
)
|
||||
|
||||
app = main_module.create_app(
|
||||
settings=settings, indexer=FakeIndexer(), provider=StubProvider()
|
||||
)
|
||||
body = TestClient(app).get("/api/status").json()
|
||||
|
||||
assert any("semântica" in error for error in body["errors"])
|
||||
assert app.state.services.embedder.name == HashEmbedder.name
|
||||
Reference in new issue
Block a user