Claude commited on
Commit
50b07b8
·
unverified ·
1 Parent(s): 6a8026b

feat(web,tests): Corr-A/B/C — exposer views + expose_alto + B2 features Web/test helper

Browse files

Audit 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.

picarones/interfaces/web/benchmark_utils.py CHANGED
@@ -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
- output_json=output_json,
 
 
 
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="standard",
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":
picarones/interfaces/web/models.py CHANGED
@@ -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__ = [
tests/_migration_helpers.py CHANGED
@@ -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,
tests/app/services/test_python_helpers.py CHANGED
@@ -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
  # ──────────────────────────────────────────────────────────────────────