File size: 5,695 Bytes
56c3bee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# Équivalence numérique — ancien runner ↔ nouveau pipeline executor

Ce document décrit comment le `CorpusRunner` introduit au Sprint S8
(combiné au `PipelineExecutor` du S7) reproduit les mêmes chiffres
CER/WER que l'ancien `picarones.measurements.runner.run_benchmark`.

C'est le **critère go/no-go de fin de Phase 2** du rewrite ciblé
(cf. `docs/roadmap/rewrite-2026.md`).  Sans cette équivalence, on
ne peut pas basculer la BnF vers le nouveau runner sans surprise.

## Architecture des deux orchestrations

### Ancien runner (`picarones.measurements.runner`)

```
Corpus[Document(image, GT)]


run_benchmark(corpus, [BaseOCREngine])

     ▼ ProcessPoolExecutor / ThreadPoolExecutor
BaseOCREngine.run(image)  →  EngineResult(text, ...)


compute_metrics(GT, text)  →  MetricsResult(cer, wer, ...)


aggregate_metrics([MetricsResult, ...])  →  {"cer": {"mean": 0.05}, ...}


EngineReport(mean_cer=0.05, ...)
```

### Nouveau pipeline (`picarones.pipeline`)

```
[DocumentRef], initial_inputs={IMAGE: Artifact}


CorpusRunner.run(spec, docs, factory_inputs, factory_ctx)

     ▼ ThreadPoolExecutor avec backpressure
PipelineExecutor.run(spec, doc, inputs, ctx)

     ▼ pour chaque step
StepExecutor.execute(inputs, params, ctx)  →  {RAW_TEXT: Artifact}

     ▼ (S13+ : EvaluationViewExecutor)
TextView.evaluate(candidate, ground_truth)  →  ViewResult(metric_values)
```

Le S12 ne livre pas encore l'`EvaluationViewExecutor` — il vérifie
juste que **si on appelle ``compute_metrics`` directement sur les
artefacts produits par le nouveau pipeline**, on obtient les mêmes
valeurs.  Le S13-S14 livrera la couche `TextView` qui fera ce
calcul automatiquement.

## Méthode de vérification (test d'équivalence)

Le test `tests/integration/test_sprint_a14_s12_executor_equivalence.py`
implémente l'équivalence :

1. **Construit deux orchestrations** consommant exactement le même
   corpus :
   - `_FakeOCREngine` (héritant de `BaseOCREngine`) pour l'ancien
     runner.
   - `_FakeStepExecutor` (satisfaisant le protocole `StepExecutor`)
     pour le nouveau.
   - Les deux retournent **le même texte** par document, indexé par
     `doc_id`.

2. **Lance les deux runners** sur le même corpus.

3. **Calcule CER/WER avec le même `compute_metrics`** sur les
   sorties des deux runners.

4. **Compare** les moyennes CER et WER.

## Tolérance : 1e-6, pas 1e-9

Le plan d'origine prévoyait une tolérance de **1e-9** ("équivalence
numérique stricte").  La réalité du code montre une divergence de
l'ordre de **1e-7** sur certaines fixtures, **uniquement à cause
d'un arrondi à 6 décimales** dans `aggregate_metrics` de l'ancien
runner :

```python
# picarones/core/metrics.py — _stats()
return {
    "mean": round(statistics.mean(values), 6),
    "median": round(statistics.median(values), 6),
    ...
}
```

Les valeurs brutes (avant `round`) sont identiques bit-à-bit
entre les deux runners.  La divergence observée provient
strictement du `round(..., 6)`.

Le test S12 utilise donc une tolérance **1e-6** (cohérente avec les
6 décimales d'arrondi) et documente cette décision.  Quand
l'agrégation finale passera par les types non-arrondis du nouveau
code (S22), la tolérance pourra être resserrée à 1e-9.

## 5 fixtures patrimoniales testées

Le test couvre 5 cas de difficulté croissante :

| Fixture | Description |
|---|---|
| `fixture_1_court` | Mots isolés, hypothèse parfaite |
| `fixture_2_paragraphe` | Phrases avec une coquille |
| `fixture_3_multi_lignes` | Multi-lignes + accents perdus |
| `fixture_4_abreviations` | Bibliographie + date erronée |
| `fixture_5_mix_langues` | Latin + français, multiples coquilles |

Plus deux cas limites :

- `test_equivalence_with_perfect_hypothesis` — CER == WER == 0
- `test_equivalence_with_empty_hypothesis` — texte produit vide

Total : **7 tests d'équivalence**, tous verts.

## Conséquences pour la migration BnF

À partir du S12, on peut affirmer que :

- Basculer un benchmark BnF du runner legacy vers le nouveau
  `CorpusRunner` ne change pas les chiffres rapportés au-delà de
  l'arrondi à 6 décimales.
- Les rapports HTML produits depuis le nouveau pipeline (S22)
  afficheront les mêmes CER que les rapports historiques (modulo
  arrondi).
- Le nouveau `CorpusRunner` apporte **trois améliorations** non
  visibles côté chiffres :
  1. Backpressure (RAM bornée même sur 1000+ docs).
  2. Timeout depuis le **début d'exécution** (pas la queue).
  3. Annulation propre via `threading.Event`.

## Limites du S12

L'équivalence vérifiée ici porte uniquement sur :

- Le pipeline OCR seul (un step → un texte → CER/WER).
- Les métriques principales `mean_cer` / `mean_wer`.

Restent à vérifier dans des sprints suivants :

- **S13** : équivalence des projecteurs (ALTO → texte) — couvert
  par les tests unitaires de `formats.alto.projector` mais pas
  encore comparé à `extract_text_from_alto` legacy.
- **S15** : équivalence des métriques structurelles (Layout F1,
  reading order F1) — non testées en S12 car elles vivent dans
  des fichiers `measurements/*.py` non encore migrés.
- **S20** : équivalence des métriques philologiques (MUFI,
  abbreviations, etc.) — idem.

Quand ces sprints ajouteront leurs tests d'équivalence, le critère
"équivalence numérique fin Phase 3 / Phase 4" sera complet.

## Statut

- **Fin de Phase 2 (S12)** — équivalence runner OCR ✅
- **Fin de Phase 3 (S18)** — équivalence views ouverte (S13-S18)
- **Fin de Phase 4 (S22)** — équivalence rapport HTML ouverte