Spaces:
Running
feat(web,tests): Corr-A/B/C — exposer views + expose_alto + B2 features Web/test helper
Browse filesAudit B3-final commit C2. Suite de C1 (CLI) — étend l'exposition
des features à la couche Web et corrige la divergence test↔prod
sur les vues identifiée par l'audit.
picarones/interfaces/web/models.py
- PipelineConfig.expose_alto: bool = False
(B5 — flag Tesseract ALTO XML)
- BenchmarkRunRequest.views: list[ViewName] = ["text_final"]
(B6 — défaut compat ascendante)
- BenchmarkRunRequest.profile: Literal[...] = "standard" (B2.6)
- BenchmarkRunRequest.partial_dir: str = "" (B2.3)
- BenchmarkRunRequest.entity_extractor: str = "" (B2.4)
- BenchmarkRunRequest.output_json: str = "" (B2.7)
picarones/interfaces/web/benchmark_utils.py
- _engine_from_competitor propage comp.expose_alto au factory
Tesseract uniquement (autres adapters ignorent).
- run_benchmark_thread_v2 propage views/profile/partial_dir/
entity_extractor/output_json à prepare_preset_args.
tests/_migration_helpers.py — helper test B4
- Nouveau kwarg views: tuple[str, ...] = ("text_final",)
propagé à prepare_preset_args.
- Corrige la divergence test↔prod identifiée par l'audit
(le helper test ne supportait pas les vues multi-format,
aucun test B4 ne couvrait ce chemin).
tests/app/services/test_python_helpers.py
- Nouveau TestMigrationHelperViewsPropagation : 1 cas qui
vérifie que le helper propage bien views au RunResult final
(text_final-only vs multi-vues).
Tests : 10 passed pour les helpers, 484 passed pour CLI+Web.
Impact utilisateur (API REST POST /api/benchmark/run) :
{
"corpus_path": "./corpus",
"competitors": [{"engine_name": "tesseract", "expose_alto": true}],
"views": ["text_final", "alto_documentary", "searchability"],
"profile": "standard"
}
→ rapport HTML avec 3 sections de vues + ALTO Tesseract.
|
@@ -251,6 +251,11 @@ def _engine_from_competitor(comp: PipelineConfig) -> Any:
|
|
| 251 |
# n'est plus possible de l'oublier pour un nouveau moteur.
|
| 252 |
try:
|
| 253 |
kwargs = _build_ocr_kwargs(engine_id, comp.ocr_model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
ocr = ocr_adapter_from_name(engine_id, **kwargs)
|
| 255 |
except ValueError as exc:
|
| 256 |
# Adapter indisponible (dépendance optionnelle absente)
|
|
@@ -384,13 +389,21 @@ def run_benchmark_thread_v2(job: BenchmarkJob, req: BenchmarkRunRequest) -> None
|
|
| 384 |
with tempfile.TemporaryDirectory(prefix="picarones_web_") as _ws:
|
| 385 |
_ws_path = Path(_ws)
|
| 386 |
_run_dir = _ws_path / "run"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
_preset = prepare_preset_args(
|
| 388 |
corpus, engines,
|
| 389 |
workspace_dir=_ws_path / "gt",
|
| 390 |
output_dir=_run_dir,
|
|
|
|
| 391 |
char_exclude=char_excl,
|
| 392 |
normalization_profile=req.normalization_profile,
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
| 394 |
)
|
| 395 |
_orch_result = RunOrchestrator(_run_dir).execute_preset(
|
| 396 |
spec=_preset.spec,
|
|
@@ -407,7 +420,7 @@ def run_benchmark_thread_v2(job: BenchmarkJob, req: BenchmarkRunRequest) -> None
|
|
| 407 |
corpus=corpus, engines=engines,
|
| 408 |
char_exclude=char_excl,
|
| 409 |
normalization_profile=req.normalization_profile,
|
| 410 |
-
profile=
|
| 411 |
)
|
| 412 |
|
| 413 |
if job.status == "cancelled":
|
|
|
|
| 251 |
# n'est plus possible de l'oublier pour un nouveau moteur.
|
| 252 |
try:
|
| 253 |
kwargs = _build_ocr_kwargs(engine_id, comp.ocr_model)
|
| 254 |
+
# Phase B3-final corr-B (mai 2026) — propage expose_alto
|
| 255 |
+
# à Tesseract (les autres adapters ignorent ce kwarg via
|
| 256 |
+
# validation du factory).
|
| 257 |
+
if comp.expose_alto and engine_id.lower() in {"tesseract", "tess"}:
|
| 258 |
+
kwargs["expose_alto"] = True
|
| 259 |
ocr = ocr_adapter_from_name(engine_id, **kwargs)
|
| 260 |
except ValueError as exc:
|
| 261 |
# Adapter indisponible (dépendance optionnelle absente)
|
|
|
|
| 389 |
with tempfile.TemporaryDirectory(prefix="picarones_web_") as _ws:
|
| 390 |
_ws_path = Path(_ws)
|
| 391 |
_run_dir = _ws_path / "run"
|
| 392 |
+
# Phase B3-final corr-A/B/C (mai 2026) — propage les
|
| 393 |
+
# nouveaux champs ``BenchmarkRunRequest`` (views, profile,
|
| 394 |
+
# partial_dir, entity_extractor, output_json).
|
| 395 |
+
_views_tuple = tuple(req.views) if req.views else ("text_final",)
|
| 396 |
_preset = prepare_preset_args(
|
| 397 |
corpus, engines,
|
| 398 |
workspace_dir=_ws_path / "gt",
|
| 399 |
output_dir=_run_dir,
|
| 400 |
+
views=_views_tuple,
|
| 401 |
char_exclude=char_excl,
|
| 402 |
normalization_profile=req.normalization_profile,
|
| 403 |
+
profile=req.profile,
|
| 404 |
+
partial_dir=req.partial_dir or None,
|
| 405 |
+
entity_extractor=req.entity_extractor or None,
|
| 406 |
+
output_json=req.output_json or output_json,
|
| 407 |
)
|
| 408 |
_orch_result = RunOrchestrator(_run_dir).execute_preset(
|
| 409 |
spec=_preset.spec,
|
|
|
|
| 420 |
corpus=corpus, engines=engines,
|
| 421 |
char_exclude=char_excl,
|
| 422 |
normalization_profile=req.normalization_profile,
|
| 423 |
+
profile=req.profile,
|
| 424 |
)
|
| 425 |
|
| 426 |
if job.status == "cancelled":
|
|
@@ -121,6 +121,17 @@ class PipelineConfig(BaseModel):
|
|
| 121 |
autorisée pour indiquer qu'aucun LLM n'est attaché au moteur OCR.
|
| 122 |
"""
|
| 123 |
prompt_file: str = Field(default="", max_length=_MAX_PROMPT_FILENAME)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
|
| 126 |
class BenchmarkRunRequest(BaseModel):
|
|
@@ -133,6 +144,33 @@ class BenchmarkRunRequest(BaseModel):
|
|
| 133 |
output_dir: str = Field(default="./rapports/", max_length=_MAX_PATH)
|
| 134 |
report_name: str = Field(default="", max_length=_MAX_NAME)
|
| 135 |
report_lang: ReportLang = "fr"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
__all__ = [
|
|
|
|
| 121 |
autorisée pour indiquer qu'aucun LLM n'est attaché au moteur OCR.
|
| 122 |
"""
|
| 123 |
prompt_file: str = Field(default="", max_length=_MAX_PROMPT_FILENAME)
|
| 124 |
+
expose_alto: bool = False
|
| 125 |
+
"""Phase B3-final corr-B (mai 2026) — active la production native
|
| 126 |
+
d'ALTO XML par Tesseract via ``pytesseract.image_to_alto_xml``.
|
| 127 |
+
|
| 128 |
+
Combiné avec ``BenchmarkRunRequest.views`` contenant
|
| 129 |
+
``alto_documentary``, débloque les sections multi-vues du rapport
|
| 130 |
+
HTML. Ignoré pour les engines non-Tesseract."""
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# Phase B3-final corr-A — vues canoniques d'évaluation acceptées.
|
| 134 |
+
ViewName = Literal["text_final", "alto_documentary", "searchability"]
|
| 135 |
|
| 136 |
|
| 137 |
class BenchmarkRunRequest(BaseModel):
|
|
|
|
| 144 |
output_dir: str = Field(default="./rapports/", max_length=_MAX_PATH)
|
| 145 |
report_name: str = Field(default="", max_length=_MAX_NAME)
|
| 146 |
report_lang: ReportLang = "fr"
|
| 147 |
+
# Phase B3-final corr-A/B/C (mai 2026) — exposition des features
|
| 148 |
+
# B2/B5/B6 aux clients de l'API REST.
|
| 149 |
+
views: list[ViewName] = Field(default_factory=lambda: ["text_final"])
|
| 150 |
+
"""Liste des vues d'évaluation à appliquer. Défaut :
|
| 151 |
+
``["text_final"]`` (compat ascendante). Pour activer le rapport
|
| 152 |
+
HTML multi-vues (AltoView, SearchView), passer ``["text_final",
|
| 153 |
+
"alto_documentary", "searchability"]``. Nécessite que les
|
| 154 |
+
pipelines produisent les artefacts éligibles (ex :
|
| 155 |
+
``alto_documentary`` requiert ``PipelineConfig.expose_alto=true``
|
| 156 |
+
côté Tesseract)."""
|
| 157 |
+
profile: Literal[
|
| 158 |
+
"minimal", "standard", "philological", "diagnostics",
|
| 159 |
+
"economics", "pipeline", "full",
|
| 160 |
+
] = "standard"
|
| 161 |
+
"""Phase B2.6 — profil de hooks document-level / corpus aggregators.
|
| 162 |
+
Sélectionne quels ``@register_document_metric`` /
|
| 163 |
+
``@register_corpus_aggregator`` s'exécutent."""
|
| 164 |
+
partial_dir: str = Field(default="", max_length=_MAX_PATH)
|
| 165 |
+
"""Phase B2.3 — répertoire pour la reprise sur interruption.
|
| 166 |
+
Vide = pas de resume."""
|
| 167 |
+
entity_extractor: str = Field(default="", max_length=_MAX_NAME * 4)
|
| 168 |
+
"""Phase B2.4 — dotted path vers une factory d'extracteur d'entités
|
| 169 |
+
(ex : ``mypkg.ner:SpacyExtractor``). Vide = pas de NER attach."""
|
| 170 |
+
output_json: str = Field(default="", max_length=_MAX_PATH)
|
| 171 |
+
"""Phase B2.7 — chemin facultatif où sérialiser le BenchmarkResult
|
| 172 |
+
legacy en JSON. Vide = pas de sortie JSON additionnelle (le
|
| 173 |
+
rapport HTML reste produit normalement)."""
|
| 174 |
|
| 175 |
|
| 176 |
__all__ = [
|
|
@@ -34,6 +34,7 @@ def run_via_orchestrator(
|
|
| 34 |
corpus: "Corpus",
|
| 35 |
engines: list[Any],
|
| 36 |
*,
|
|
|
|
| 37 |
char_exclude: Any | None = None,
|
| 38 |
normalization_profile: Any | None = None,
|
| 39 |
output_json: str | Path | None = None,
|
|
@@ -97,6 +98,7 @@ def run_via_orchestrator(
|
|
| 97 |
corpus, engines,
|
| 98 |
workspace_dir=ws_path / "gt",
|
| 99 |
output_dir=run_dir,
|
|
|
|
| 100 |
char_exclude=char_exclude,
|
| 101 |
normalization_profile=normalization_profile,
|
| 102 |
partial_dir=partial_dir,
|
|
|
|
| 34 |
corpus: "Corpus",
|
| 35 |
engines: list[Any],
|
| 36 |
*,
|
| 37 |
+
views: tuple[str, ...] = ("text_final",),
|
| 38 |
char_exclude: Any | None = None,
|
| 39 |
normalization_profile: Any | None = None,
|
| 40 |
output_json: str | Path | None = None,
|
|
|
|
| 98 |
corpus, engines,
|
| 99 |
workspace_dir=ws_path / "gt",
|
| 100 |
output_dir=run_dir,
|
| 101 |
+
views=views,
|
| 102 |
char_exclude=char_exclude,
|
| 103 |
normalization_profile=normalization_profile,
|
| 104 |
partial_dir=partial_dir,
|
|
@@ -104,6 +104,43 @@ class TestNominal:
|
|
| 104 |
)
|
| 105 |
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
# ──────────────────────────────────────────────────────────────────────
|
| 108 |
# Multi-engines
|
| 109 |
# ──────────────────────────────────────────────────────────────────────
|
|
|
|
| 104 |
)
|
| 105 |
|
| 106 |
|
| 107 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 108 |
+
# Phase B3-final corr-A/D — vérifier que le helper test propage `views`
|
| 109 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class TestMigrationHelperViewsPropagation:
|
| 113 |
+
"""Garantie que ``tests/_migration_helpers.run_via_orchestrator``
|
| 114 |
+
propage le param ``views`` à ``prepare_preset_args``.
|
| 115 |
+
|
| 116 |
+
Audit Phase B3-final a identifié une divergence test↔prod : le
|
| 117 |
+
helper de test ne transmettait pas ``views``, donc aucun test B4
|
| 118 |
+
ne couvrait le multi-vues via le helper. Corr-D : helper test
|
| 119 |
+
aligné, test de propagation explicite.
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
def test_helper_propagates_views_to_run_result(
|
| 123 |
+
self, tmp_path: Path,
|
| 124 |
+
) -> None:
|
| 125 |
+
from tests._migration_helpers import run_via_orchestrator
|
| 126 |
+
|
| 127 |
+
corpus = _make_corpus(tmp_path, n=1)
|
| 128 |
+
engine = _MockOCR()
|
| 129 |
+
|
| 130 |
+
# Sans param ``views`` → défaut text_final seulement.
|
| 131 |
+
bm_default = run_via_orchestrator(corpus, [engine])
|
| 132 |
+
assert "text_final" in bm_default.view_results
|
| 133 |
+
assert "alto_documentary" not in bm_default.view_results
|
| 134 |
+
|
| 135 |
+
# Avec ``views=...`` → propagation effective.
|
| 136 |
+
bm_multi = run_via_orchestrator(
|
| 137 |
+
corpus, [engine],
|
| 138 |
+
views=("text_final", "searchability"),
|
| 139 |
+
)
|
| 140 |
+
assert "text_final" in bm_multi.view_results
|
| 141 |
+
assert "searchability" in bm_multi.view_results
|
| 142 |
+
|
| 143 |
+
|
| 144 |
# ──────────────────────────────────────────────────────────────────────
|
| 145 |
# Multi-engines
|
| 146 |
# ──────────────────────────────────────────────────────────────────────
|