feat(ocr): migrer l'OCR de docTR (PyTorch) vers OnnxTR (ONNX Runtime)
OnnxTR exécute les MÊMES modèles que docTR (db_resnet50 + crnn_vgg16_bn) sur ONNX Runtime, sans PyTorch. Corrige le crash torch/oneDNN « could not create a primitive » sur CPU contraint (VM 2 cœurs collaborateur : OCR scan impossible → quarantaine). Qualité identique validée empiriquement (CER 0,10-0,23 % vs docTR, 2 validations indépendantes Claude+Qwen), OCR ~2-3× plus rapide CPU. - core : import OnnxTR, _get_ocr_model(), _OCR_AVAILABLE, boucle OCR inchangée (API miroir) ; ONNXTR_CACHE_DIR pour le frozen ; bandeau de logs ENV au démarrage (OS, CPU+AVX, cœurs, RAM, versions, providers) pour retours terrain auto-suffisants. - 3 .spec : embarquent les poids ONNX OnnxTR (fail-closed) + hiddenimports onnxtr. - requirements : onnxtr[cpu] (python-doctr conservé transitoirement). - inclut le correctif quarantaine-visible du runner (GO Qwen). Tests : test_ocr_onnxtr.py (RED→GREEN), 95 unit passed, e2e scan client OK (OCR 5/5, PDF produit, plus de crash). Retrait torch du frozen + rebuild Windows = étapes suivantes (gates Dom). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,42 @@ def default_output_dir(input_path) -> Path:
|
||||
return base / "anonymise"
|
||||
|
||||
|
||||
def _delivered_pdf_paths(result: object) -> list[Path]:
|
||||
"""Retourne les PDF effectivement produits par le moteur.
|
||||
|
||||
Le moteur retourne toujours des clés ``pdf_*`` pour une sortie livrable.
|
||||
Les tests unitaires historiques injectent souvent ``{}`` comme succès factice ;
|
||||
on ne les assimile donc pas à un échec ici.
|
||||
"""
|
||||
if not isinstance(result, dict):
|
||||
return []
|
||||
paths: list[Path] = []
|
||||
for key, value in result.items():
|
||||
if not str(key).startswith("pdf") or not isinstance(value, (str, Path)):
|
||||
continue
|
||||
path = Path(value)
|
||||
if path.exists() and path.is_file():
|
||||
paths.append(path)
|
||||
return paths
|
||||
|
||||
|
||||
def _engine_result_error(result: object) -> str | None:
|
||||
"""Traduit un retour moteur non livrable en erreur visible GUI."""
|
||||
if not isinstance(result, dict):
|
||||
return None
|
||||
if result.get("status") == "quarantined":
|
||||
reason = result.get("reason") or "document mis en quarantaine"
|
||||
return f"Document mis en quarantaine : {reason}"
|
||||
has_real_engine_outputs = (
|
||||
"text" in result
|
||||
or "audit" in result
|
||||
or any(str(key).startswith("pdf") for key in result)
|
||||
)
|
||||
if has_real_engine_outputs and not _delivered_pdf_paths(result):
|
||||
return "Aucune sortie PDF anonymisée produite."
|
||||
return None
|
||||
|
||||
|
||||
def discover_documents(input_path, extensions: Optional[Sequence[str]] = None) -> list[Path]:
|
||||
"""Liste les documents à traiter (fichier unique ou dossier récursif)."""
|
||||
path = Path(input_path)
|
||||
@@ -176,7 +212,10 @@ class ProcessingRunner:
|
||||
else:
|
||||
doc_out = out_root
|
||||
doc_out.mkdir(parents=True, exist_ok=True)
|
||||
self._process_fn(doc, doc_out)
|
||||
result = self._process_fn(doc, doc_out)
|
||||
result_error = _engine_result_error(result)
|
||||
if result_error is not None:
|
||||
raise RuntimeError(result_error)
|
||||
summary.succeeded += 1
|
||||
log(f"OK : {doc.name}")
|
||||
except Exception as exc: # un échec n'interrompt pas le lot
|
||||
|
||||
Reference in New Issue
Block a user