File size: 52,966 Bytes
48b0757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b782d0
48b0757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b782d0
48b0757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b782d0
48b0757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b782d0
48b0757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
# Plan d'évolution Picarones — 2026

> Synthèse d'une conversation de cadrage (avril 2026) entre l'équipe Picarones
> et un LLM externe. Couvre à la fois l'enrichissement du benchmark texte
> existant (axe A) et la bascule progressive vers un banc d'essai de pipelines
> composées (axe B), pensés comme **un seul produit à deux modes d'usage**.
>
> Ce document est un plan d'intention, pas une spécification figée. Chaque
> sprint réel devra rouvrir et challenger les hypothèses ci-dessous, en
> particulier celles de la Phase B qui dépendent du retour terrain BnF.

---

## 1. Principe directeur

### 1.1 Un seul produit, deux modes d'usage

Picarones reste **une seule base de code, un seul rapport, un seul runner**.
La distinction entre « benchmark texte » (axe A) et « banc d'essai de
pipelines composées » (axe B) n'est pas un fork du produit, c'est un
continuum :

> **Une pipeline à un seul module est un cas particulier d'une pipeline à
> N modules.**

Cette règle, déjà appliquée avec succès en Sprint 3 quand `OCRLLMPipeline`
a hérité de `BaseOCREngine`, doit être poussée un cran plus loin. Le mode
benchmark texte devient alors le **cas dégénéré** du banc d'essai
pipelines.

### 1.2 Trois modes d'entrée, un seul moteur

| Mode | Commande | Pour qui |
|---|---|---|
| **Legacy** | `picarones run --corpus ./c --engines tesseract,pero` | Chercheur qui n'a jamais entendu parler de pipelines. Aucune nouvelle option à apprendre. |
| **YAML composé** | `picarones pipeline run my-pipeline.yaml` | Ingénieur qui compose une chaîne OCR → reconstructeur → post-correcteur. |
| **Hybride** | `picarones run --pipeline-yaml post.yaml --engines tesseract,pero` | Qui veut comparer l'effet du choix d'OCR à pipeline post-OCR fixée. |

En interne, **les trois construisent la même structure** : un graphe orienté
de modules avec, pour le mode legacy, un graphe trivial à un seul nœud
par moteur.

### 1.3 Rapport adaptatif par défaut

Question à se poser à chaque vue : « si la pipeline a un seul nœud, est-ce
que cette vue apporte quelque chose ? ».

- **Non** → la vue est **absente** (pas vide, pas désactivée).
- **Oui** → la vue est affichée, éventuellement dégradée mais utile.

Application directe du principe du panneau « Avancé » du Sprint 21 :
l'utilisateur simple ne voit pas ce qu'il n'a pas demandé. La discipline du
masquage automatique est ce qui distingue un bon outil d'une usine à gaz.

### 1.4 La règle pratique à tenir

À chaque décision de design : *« est-ce que ça oblige l'utilisateur du mode
simple à apprendre quelque chose de nouveau ? »*. Si oui, mauvaise voie.
Repenser jusqu'à ce que la réponse soit non.

### 1.5 Ce qui ne change pas

Le différenciateur qui distingue déjà Picarones reste central : **rigueur
méthodologique, traçabilité, absence de prescription**. Tout ce que ce plan
ajoute doit servir cette ligne, pas la diluer. En particulier :

- Pas de LLM dans le chemin critique du rapport.
- Pas de score composite imposé. Tout score agrégé est opt-in et
  étiqueté comme tel.
- Chaque chiffre rendu dans la synthèse factuelle reste traçable au
  payload du `Fact` source (garde-fou anti-hallucination du Sprint 19).

---

## 2. Phase 0 — Fondation commune

Trois chantiers à mener avant le reste, sans lesquels tout le plan construit
sur du sable. Ils débloquent à la fois l'axe A (métriques structurelles,
philologiques) et l'axe B (pipelines composées).

### 2.1 Modèle de données multi-niveaux

**Pourquoi maintenant.** La GT actuelle est mono-niveau (texte plat dans
`.gt.txt`). Cette contrainte interdit l'évaluation de tout module qui
produit ou consomme une autre représentation : ALTO, PAGE XML, entités
nommées, ordre de lecture. C'est le verrou qui empêche tout l'axe B et qui
limite déjà des métriques de l'axe A (Layout F1, reading order F1).

**Refonte de `picarones/domain/corpus.py`.** La classe `Document` actuelle
porte `image_path`, `ground_truth: str`, `ocr_text`. La nouvelle version
porte une GT structurée :

```python
class GTLevel(str, Enum):
    TEXT = "text"
    ALTO = "alto"
    PAGE = "page"
    ENTITIES = "entities"
    READING_ORDER = "reading_order"

@dataclass
class Document:
    image_path: Path
    ground_truths: dict[GTLevel, GTPayload]
    metadata: dict
```

Chaque payload est typé : `TextGT(str)`, `AltoGT(xml_root)`,
`EntitiesGT(list[Entity])`, etc. Le chargeur `load_corpus_from_directory`
détecte automatiquement les fichiers présents (`.gt.txt`, `.gt.alto.xml`,
`.gt.page.xml`, `.gt.entities.json`) et peuple les niveaux disponibles.

**Compatibilité ascendante stricte.** Un corpus avec uniquement `.gt.txt`
doit continuer à fonctionner exactement comme avant. Le runner consulte
`document.ground_truths[GTLevel.TEXT]`. Une `@property ground_truth` de
transition garantit zéro casse côté tests.

**Conséquences à anticiper.** Les fixtures (`fixtures.py`) doivent
générer des corpus mixtes : text-only et text+alto, pour que la suite de
tests valide les deux régimes. Le rapport HTML doit afficher dans la vue
Document quels niveaux de GT sont disponibles.

**Critère de réussite.** Les 1242 tests actuels passent sans modification.
Trois nouveaux tests valident le chargement d'un corpus avec GT ALTO
partielle (certains documents ont l'ALTO, d'autres non).

**Effort estimé.** Un sprint. Risque : les imports (HuggingFace, IIIF,
HTR-United) peuvent nécessiter de petits ajustements pour produire la
nouvelle structure.

### 2.2 Interface module générique

**Pourquoi maintenant.** Aujourd'hui `BaseOCREngine` est typé `image →
texte` de manière implicite. Pour qu'un même runner puisse exécuter un
mappeur ALTO ou un rewriter, il faut une interface plus générale dont
`BaseOCREngine` devient un cas particulier.

**Création de `picarones/domain/module_protocol.py`.**

```python
class ArtifactType(str, Enum):
    IMAGE = "image"
    TEXT = "text"
    ALTO = "alto"
    PAGE = "page"
    ENTITIES = "entities"
    READING_ORDER = "reading_order"

class BaseModule:
    name: str
    input_types: tuple[ArtifactType, ...]
    output_types: tuple[ArtifactType, ...]
    execution_mode: Literal["io", "cpu"]

    def process(self, inputs: dict[ArtifactType, Any]) -> dict[ArtifactType, Any]:
        raise NotImplementedError

    def metadata(self) -> dict: ...
```

Un OCR classique déclare `input_types=(IMAGE,)`, `output_types=(TEXT,)`.
Un mappeur VLM→ALTO déclare `input_types=(IMAGE,)`,
`output_types=(TEXT, ALTO)`. Un rewriter ALTO post-correction déclare
`input_types=(ALTO,)`, `output_types=(ALTO,)`.

**`BaseOCREngine` devient un alias de compatibilité** dont l'implémentation
de `process` wrappe l'ancien `_run_ocr` et retourne `{TEXT: result.text}`.
Aucun adaptateur OCR n'est touché à ce stade.

**Critère de réussite.** Tous les moteurs existants passent les tests sans
modification. Un test ajouté instancie un `MockModule` qui consomme `TEXT`
et produit `ALTO`, et vérifie que le runner peut l'invoquer.

**Effort estimé.** Un demi-sprint, principalement de la rigueur de typage.

### 2.3 Métriques composables

**Pourquoi maintenant.** Aujourd'hui une métrique est calculée une fois,
en sortie de moteur, sur la paire `(GT_text, hypothesis_text)`. Dans une
pipeline composée, on veut calculer une métrique **à chaque jonction** du
DAG, et la métrique change selon les types d'artefacts à la jonction.

**Registre de métriques typé.** Chaque métrique déclare ses types d'entrée :

```python
@register_metric(input_types=(TEXT, TEXT))
def cer(reference: str, hypothesis: str) -> float: ...

@register_metric(input_types=(ALTO, ALTO))
def reading_order_f1(ref_alto, hyp_alto) -> float: ...

@register_metric(input_types=(TEXT, ALTO))
def text_preservation_after_reconstruction(ref_text, hyp_alto) -> float: ...
```

Le runner, étant donné une jonction `(artifact_produced, gt_at_this_level)`,
sélectionne automatiquement les métriques applicables et les calcule.
C'est le mécanisme qui rend l'axe B possible.

**Compatibilité.** Tout `compute_metrics` actuel devient un appel orchestré
du registre. Le format de sortie de `MetricsResult` reste identique pour
les jonctions `text→text`.

**Critère de réussite.** Les rapports HTML existants restent strictement
identiques (déterminisme bit-à-bit) sur les fixtures de test. Un nouveau
test enregistre une métrique factice `(TEXT, ALTO)` et vérifie qu'elle est
sélectionnée à la bonne jonction.

**Effort estimé.** Un demi-sprint.

### 2.4 Bilan Phase 0

À la fin de cette phase :

- Tous les tests existants passent sans modification.
- Le rapport HTML est strictement identique sur les fixtures legacy.
- L'infrastructure peut accueillir des modules non-OCR, des GT
  multi-niveaux et des métriques typées par jonction.

**Aucune autre étape ne peut commencer tant que la Phase 0 n'est pas
stable.** Trois sprints consécutifs, sans dérive de scope.

---

## 3. Phase A — Enrichissement métrique et UX

L'axe A travaille sur le périmètre actuel (benchmark texte mono-pipeline)
et bénéficie immédiatement à tous les utilisateurs existants. Il se
décline en deux familles : **A.I — adresser les 9 critiques structurelles
identifiées dans la conversation de cadrage**, et **A.II — combler les
métriques absentes**. Une vue transversale (A.III stratification) clôt
l'axe.

### A.I — Réponses aux 9 critiques structurelles

Chaque sous-section nomme la critique, ce qui existe déjà, et le chantier
à mener.

#### A.I.1 — La granularité ne s'arrête plus à la page

**Critique.** CER/WER agrégés au document, agrégés au moteur. Le niveau
ligne existe (`line_metrics.py`, Gini, percentiles depuis Sprint 10) mais
n'est que décrit. Le niveau token rare est absent.

**Chantier 1 — Vue « Worst lines globale ».** Une nouvelle vue dans le
rapport qui transcende les documents : top-N lignes les plus mal
transcrites de tout le corpus, classées par CER ligne, avec image de la
ligne (extrait ALTO si disponible, sinon crop image), GT et hypothèse en
diff coloré, et lien vers le document parent. C'est le complément
opérationnel du percentile p95 abstrait.

Paramètres : N (défaut 20), filtre par moteur, filtre par strate
`script_type`. Les ingrédients existent (`line_metrics.py`,
`diff_utils.py`, vue galerie) — il manque la requête transversale et la
vue dédiée.

**Chantier 2 — Métrique « rare-token recall ».** Calcul du rappel sur les
tokens GT de fréquence ≤ 2 dans le corpus (hapax + dis legomena).
Implémentation : tokenisation Unicode-aware sur la GT, comptage de
fréquence corpus-wide, calcul du taux de tokens rares correctement
restitués par chaque moteur. Une variante stratifiée par `pos`-tag ou par
catégorie (NOUN, PROPN) est rendue par A.II.1 (NER).

Conjecture à tester sur la fixture : cette métrique discrimine plus les
moteurs que le CER global. Si confirmée, elle gagne sa place dans le
tableau de classement principal à côté du CER.

#### A.I.2 — La médiane devient le critère de classement par défaut

**Critique.** Le rapport affiche les deux mais classe sur la moyenne. Sur
des distributions asymétriques (80 % à 3 %, 20 % à 40 %), le moteur A peut
gagner sur la moyenne uniquement parce qu'il rate moins souvent les pages
catastrophiques, alors que B est strictement meilleur sur le quotidien.

**Chantier.** Le tri par défaut du tableau de classement passe sur la
**médiane CER**. La moyenne devient une colonne secondaire. Un
**avertissement** s'affiche automatiquement si l'écart relatif
`|moyenne − médiane| / médiane` dépasse un seuil (par défaut 30 %), avec
un message explicite : *« la distribution des CER est très asymétrique
sur ce corpus — la moyenne est tirée par quelques documents
catastrophiques. La médiane est plus représentative. »*

Cohérence supplémentaire : le test de Friedman travaille déjà sur les
rangs (Sprint 18). Trier sur la médiane aligne la lecture humaine sur la
lecture statistique.

#### A.I.3 — Vue « inhabituel ici » alimentée par l'historique SQLite

**Critique.** Le rapport décrit le corpus évalué mais ne le compare jamais
à une distribution de référence. L'historique SQLite (Sprint 8) existe
mais aucun détecteur narratif ne le lit.

**Chantier 1 — Encart « Ce corpus est-il habituel ? ».** En tête du
rapport, sous la synthèse factuelle, un mini-graphique qui place le score
de difficulté moyen du corpus courant sur la distribution des corpus
précédents stockés en SQLite (boxplot + point). Une phrase factuelle :
*« Difficulté observée 0,62 — au 88ᵉ percentile des 47 corpus précédents
de votre institution. Ce corpus est plus difficile que la moyenne. »*

**Chantier 2 — Détecteur narratif `engine_off_baseline`.** Pour chaque
moteur, comparer le CER observé à la moyenne historique des derniers
benchmarks. Templates FR/EN du type : *« Tesseract a obtenu 5,2 % CER ici,
vs 4,1 % en moyenne sur les 12 derniers benchmarks de votre institution.
Ce corpus lui est plus difficile que vos corpus habituels. »*

Garde-fous : ne déclenche que si N_runs ≥ 5 et même `corpus_signature`
(empreinte de strates). Sinon le détecteur reste silencieux.

#### A.I.4 — Taxonomie d'erreurs exploitée à 3 niveaux

**Critique.** 10 classes, mais le rapport montre un seul histogramme.

**Chantier 1 — Co-occurrence pattern.** Matrice de co-occurrence des
classes taxonomiques au niveau document. Si `ligature_error` et
`abbreviation_error` co-occurrent toujours, c'est un signal de scribe
particulier — utile pour stratifier le corpus *a posteriori*.
Visualisation : heatmap de Jaccard entre paires de classes.

**Chantier 2 — Évolution intra-document.** Étendre la heatmap de CER par
tranche de page (déjà disponible) à toutes les classes taxonomiques.
Permet de distinguer les erreurs de marge (concentrées en bordure) des
erreurs de scribe (uniformes).

**Chantier 3 — Taxonomie comparative.** Vue côte-à-côte des profils
taxonomiques de deux moteurs avec CER similaire. Diagramme en miroir
(barres horizontales) qui montre que A fait surtout des erreurs de casse
(récupérables) tandis que B fait des lacunes (irrécupérables). Le
détecteur `error_profile_outlier` existe mais ne génère pas cette vue
comparative — il faut la créer.

#### A.I.5 — Normalisation diplomatique en curseur fin

**Critique.** `cer_diplomatic` applique un profil entier. Un éditeur peut
vouloir « je tolère ſ=s mais pas u=v ».

**Chantier.** Le panneau « Avancé » (Sprint 21) gagne une section
**« Équivalences diplomatiques »** : liste de toutes les transformations
des profils (`medieval_french`, `early_modern_french`, etc.) sous forme
de cases à cocher granulaires. Quand l'utilisateur (dé)coche, le CER se
recalcule **côté client** (les hypothèses et GT pré-tokenisées sont déjà
embarquées) et le tableau de classement se met à jour en direct.

État persisté en URL (`?eq=oe-ae,longs-s,...`) comme les autres options
du panneau Avancé. Garde-fou : ne pas recalculer plus d'une fois par
seconde (debounce).

C'est précisément ce qui distingue un benchmark *outil de mesure* d'un
*outil de décision éditoriale*.

#### A.I.6 — Coût projeté en volume cible

**Critique.** La vue Pareto (Sprint 20) trace CER vs coût mais le coût
n'est pas linéaire en valeur — payer 50 € de plus sur 50 pages est
trivial, sur 5 millions ça change tout.

**Chantier.** Champ « **Volume cible** » dans le panneau Avancé : nombre
de pages que l'utilisateur projette de traiter en production. La vue
Pareto et le tableau Pareto recalculent le **coût total projeté** :

> *« Pour vos 80 000 pages BMS — Tesseract = 3 €, Pero = 0 € (local),
> Mistral OCR = 280 €, GPT-4o post-correction = 600 €. »*

Trois axes de la vue Pareto restent disponibles (coût, vitesse, carbone)
mais le coût se lit en valeur projetée plutôt qu'en valeur unitaire. Le
détecteur `cost_outlier` (Sprint 20) gagne un seuil paramétré par
volume — ce qui est trivial à 50 pages devient bloquant à 5 M.

#### A.I.7 — Sur-normalisation LLM en vue analytique dédiée

**Critique.** La classe taxonomique 10, le détecteur d'hallucination, la
vue triple-diff document existent. Mais le score agrégé (« 0,05 % ») ne
dit rien sur **quoi** corriger dans le prompt.

**Chantier.** Nouvelle vue **« Modernisation lexicale »** dédiée aux
moteurs LLM/VLM. Tableau de fréquences :

| Forme historique GT | Forme « modernisée » par le LLM | Occurrences GT | % de fois modernisé |
|---|---|---:|---:|
| maistre | maître | 47 | 85 % |
| nostre | nostre (préservé) | 92 | 8 % |
| veoir | voir | 23 | 100 % |

Calcul : alignement caractère par caractère GT/hypothèse, extraction des
substitutions sur tokens GT non présents dans un dictionnaire moderne
(stop-list paramétrable). Trie les substitutions les plus fréquentes par
moteur.

C'est exploitable pour ajuster le prompt — bien plus utile qu'un score
agrégé.

#### A.I.8 — Robustesse synthétique projetée sur le corpus réel

**Critique.** Le module `robustness.py` génère des dégradations
synthétiques (bruit, flou, rotation). Mais aucun lien avec les
caractéristiques réelles des images.

**Chantier.** Pour chaque document du corpus, mesurer (via
`image_quality.py`) les niveaux de bruit/flou/contraste réels. Projeter
ces niveaux sur la courbe de robustesse synthétique pour estimer le
**déficit attendu de CER** :

> *« 30 % de vos documents ont un bruit équivalent à σ=15 où Tesseract
> perd 8 points de CER — soit un déficit attendu global de 2,4 points. »*

Visualisation : sur le graphique de robustesse, ajouter un histogramme
des qualités réelles observées en arrière-plan. Le détecteur
`robustness_fragile` (Sprint 19) est étendu pour intégrer ce déficit
projeté dans son texte.

#### A.I.9 — Section « Leviers d'amélioration »

**Critique.** Le rapport dit « voici les performances » mais jamais
« voici ce qui changerait facilement le classement ».

**Chantier.** Nouvelle section en pied de rapport, factuelle, qui agrège
des observations actionnables à partir des données déjà calculées :

- *« 45 % des erreurs de Tesseract sont de la classe `casse` —
  applicable post-traitement gratuit. »* (source : taxonomie)
- *« 12 documents concentrent 60 % du CER total — leur rescan en haute
  résolution aurait plus d'impact que de changer de moteur. »* (source :
  distribution par document + `image_quality`)
- *« Mistral OCR et Pero OCR sont complémentaires : leurs erreurs ne se
  chevauchent qu'à 30 %. Un vote majoritaire entre les deux ferait
  passer le CER de 7 % à 4 %. »* (source : Venn d'erreurs +
  `cer_oracle`, voir A.II.8)

Format : liste de cartes, chacune avec un titre court, un chiffre saillant
et un lien vers la vue détaillée qui justifie l'observation.

Architecture : un nouveau registre `levers/` parallèle au registre des
détecteurs narratifs, avec la même contrainte de traçabilité des chiffres
(garde-fou anti-hallucination).

### A.II — Métriques absentes à ajouter

Classées par utilité réelle (haut ROI d'abord). Trois métriques marquent
le premier livrable parce qu'elles ouvrent une dimension d'utilité
nouvelle dans le rapport.

#### A.II.1 — Trois métriques prioritaires (premier livrable)

**A.II.1.a — Précision sur entités nommées (NER).**

Nouveau module `picarones/evaluation/metrics/ner.py`. Backends : spaCy multilingue,
Stanza, modèle HIPE pour les corpus historiques. Choix paramétré par
profil (`fr_core_news_lg`, `xx_ent_wiki_sm`, `hipe2022`).

Métriques calculées sur la paire `(entities_GT, entities_OCR)` après
alignement par chevauchement de spans :

- Précision, rappel, F1 par catégorie (`PER`, `LOC`, `ORG`, `DATE`, `MISC`)
- F1 global pondéré
- Taux d'**hallucinations d'entité** : entités en OCR sans
  correspondance GT

Le rapport gagne une vue dédiée : tableau moteur × catégorie, drill-down
sur les `PER` ratés. C'est la métrique qui parle directement aux
indexeurs et aux prosopographes.

**Piège.** Les modèles NER hallucinent eux-mêmes. La métrique mesure
conjointement OCR + NER. Documenter explicitement ce biais dans la
glossaire (entrée `ner_score`).

**A.II.1.b — Score de calibration des moteurs.**

Nouveau module `picarones/evaluation/metrics/calibration.py`. Tous les moteurs cibles
fournissent une confidence par token ou par ligne (Tesseract `tsv`
output, Pero OCR via `PageLayout`, Mistral OCR via `confidence`, Google
Vision via `Word.confidence`). Ajout d'un champ
`EngineResult.token_confidences: list[float]`.

Métriques :

- **ECE** (Expected Calibration Error) en 10 bins
- **MCE** (Maximum Calibration Error)
- **Reliability diagram** en SVG embarqué dans le rapport

Vue « Fiabilité de l'auto-évaluation » qui répond à *« quand le moteur
dit qu'il est sûr, est-il vraiment sûr ? »*. Pour un workflow de
validation humaine, c'est la différence entre vérifier 100 % vs 15 % du
corpus.

**Piège.** Les confidences VLM/LLM sont moins fiables et moins
standardisées (logprobs vs scores arbitraires). Marquer explicitement les
moteurs sans calibration calculable.

**A.II.1.c — Divergence taxonomique entre moteurs.**

Extension de `core/taxonomy.py`. À partir des distributions taxonomiques
agrégées (déjà calculées), ajout de la KL-divergence et de la
Jensen-Shannon-divergence pour chaque paire de moteurs.

Le rapport gagne une **matrice de divergence triangulaire**. Une
divergence élevée signale des moteurs spécialisés sur des erreurs
différentes — candidats pour un voting ensemble. Faible divergence =
mêmes faiblesses, pas de gain attendu.

Couplé au score de complémentarité quantifiée (A.II.8.a).

Détecteur narratif `ensemble_opportunity` qui remonte automatiquement :
*« Pero et Mistral sont fortement complémentaires (KL=0,82) — un voting
majoritaire pourrait faire passer le CER de 7 % à 4 %. »*

**Effort total A.II.1.** Un sprint et demi pour les trois métriques +
leurs vues + la mise à jour du moteur narratif et du glossaire.

#### A.II.2 — Métriques structurelles

Ces métriques ne sont calculables que quand la GT ALTO/PAGE est
disponible (Phase 0.1).

**Reading order F1 par région.** Implémentation de la métrique ICDAR 2015
(Antonacopoulos). Comparaison de l'ordre des `TextRegion` dans la GT et
dans l'hypothèse ALTO. F1 sur les paires consécutives correctement
préservées. Pour les pipelines qui produisent du texte plat, l'ordre est
extrait de l'ordre de concaténation (et la métrique signale cette
extraction comme approximative).

**Layout F1 par type de région.** Précision/rappel sur la détection de
`TextRegion`, `MarginNote`, `Header`, `Footer`, `Drop-Cap`. Métrique
critique pour les manuscrits glosés et les journaux multi-colonnes. Vue
qui répond à *« le moteur sépare-t-il bien le texte principal de la
glose ? »*.

**Différence de score Flesch.** Calcul du score Flesch (ou
Flesch-Kincaid) sur GT et sortie OCR. La différence est un signal
d'over-normalisation indépendant de toute classe taxonomique. **N'exige
aucun alignement**, donc fiable même quand l'OCR est très dégradé.
Particulièrement utile pour repérer les LLM qui « lissent » la langue
historique.

#### A.II.3 — Métriques philologiques

Pour les corpus médiévaux et les éditions critiques.

**Précision par bloc Unicode.** À partir de la matrice de confusion
existante, agrégation par bloc : *Latin de Base* (U+0000–U+007F),
*Latin-1 Supplément*, *Latin Étendu A et B*, *Diacritiques combinants*,
*Présentation latine*. Graphe à barres groupées qui dit immédiatement
*« ce moteur restitue 95 % du Latin de Base mais 12 % des formes de
présentation latines »*. Actionnable en un coup d'œil pour le choix
éditorial.

**Score d'expansion d'abréviations en deux variantes.** Reconnaissance
des abréviations médiévales par leur position Unicode (ꝑ, ꝓ, ꝗ, p̃, q̃)
et calcul de :

- **Strict** : taux de préservation de la forme abrégée Unicode
- **Expansion** : taux de développement correct (ꝑ → per, ꝓ → pro)

Le ratio des deux dit beaucoup sur la convention adoptée par le moteur.
Crucial pour distinguer un OCR diplomatique d'un OCR modernisant.

**Score de couverture MUFI.** Liste de référence : caractères MUFI v4.0
utilisés dans le GT. Mesure : taux de ces caractères correctement
préservés dans l'OCR. Pour les médiévistes, c'est un critère éditorial
central — déjà mentionné dans le glossaire (Sprint 21) mais non mesuré.

#### A.II.4 — Métriques de fiabilité

**Inter-annotator agreement.** Quand le corpus a plusieurs GT (par
exemple deux paléographes), calcul du Cohen κ ou Krippendorff α. Donne le
plafond atteignable. Affiché dans la synthèse factuelle :

> *« Le CER moyen de Pero (4,2 %) approche le plafond humain pour ce
> corpus (κ = 0,89). »*

Nécessite une extension du loader pour accepter `doc_001.gt.A.txt` et
`doc_001.gt.B.txt` comme GT multiples — ce qui s'inscrit naturellement
dans le modèle multi-niveaux de la Phase 0.1.

**Score de stabilité multi-runs.** Le runner gagne une option
`--repeats N` qui exécute N fois la même pipeline LLM sur les mêmes
documents et mesure :

- Variance du CER
- Taux de tokens divergents entre runs
- Divergence sémantique (BERTScore sur paires de sorties)

C'est critique pour la reproductibilité scientifique. Une publication qui
rapporte un CER LLM sans stabilité est méthodologiquement faible.
Détecteur narratif `engine_unstable` qui remonte les moteurs dont la
variance dépasse un seuil.

#### A.II.5 — Métriques d'utilisabilité aval

**OCR-friendliness pour la recherche plein-texte.** Pourcentage de mots
GT dont une variante orthographiquement proche (Levenshtein ≤ 2) existe
dans la sortie OCR. C'est ce que recherchent réellement Elastic et Solr
en mode fuzzy. Un CER de 8 % peut donner 95 % de findability si les
erreurs sont concentrées sur des caractères non-significatifs.

Affichée à côté du CER dans le tableau principal sous le nom
**« Recherchabilité »**.

**Précision sur séquences numériques.** Extraction par regex des dates
(formats classiques et historiques : MDCLXVIII, *mil cinq cens*, 1ᵉʳ
janvier 1789), foliotation, montants, années régnales. Mesure de
précision sur ces séquences. Pour un économiste-historien ou un éditeur
de chartes, c'est un proxy direct de la qualité éditoriale.

#### A.II.6 — Métriques de processus et économiques

**Throughput effectif.**

```
pages_par_heure_utilisable =
    pages_traitées / (durée_totale + temps_correction_humaine_estimé)
```

Le temps de correction humaine est estimé par
`temps_par_erreur × nombre_d_erreurs`. La constante `temps_par_erreur`
est paramétrable (défaut 5 secondes par erreur de mot, justifié par les
études HTR-United).

Discrimine fortement entre un cloud rapide à 30 % de timeouts et un local
lent à 100 % de fiabilité.

**Coût marginal par erreur évitée.** Extension naturelle de la vue
Pareto. Pour chaque paire de moteurs :

```
coût_marginal = (coût_B − coût_A) / (errors_A − errors_B)   # quand B est meilleur
```

Nouvelle colonne dans le tableau Pareto : *« Passer de Tesseract à
Mistral OCR : 0,83 € par erreur évitée »*. Couplé au volume cible (chantier
A.I.6), devient un outil de décision business.

#### A.II.7 — Métriques d'image prédictives

**Score de complexité paléographique.** Trois features simples
combinées :

- Densité de glyphes par cm² (à partir d'OCR amont approximatif)
- Variabilité de hauteur de ligne (écart-type sur les lignes détectées)
- Ratio encre/papier (Otsu)

Combinaison pondérée en un score [0, 1]. Corrélé avec le CER attendu
sur le corpus. Permet d'expliquer une partie de la difficulté observée.

**Score d'homogénéité du corpus.** Variance des features image entre
documents. Score bas = corpus uniforme = moyenne fiable. Score haut =
corpus hétérogène = la moyenne ment, il faut stratifier.

Avertissement automatique en haut de la vue Classement quand le score
d'homogénéité est bas : *« ⚠ Ce corpus est hétérogène (score 0,34) — la
moyenne globale masque des disparités importantes. Voir la vue
stratifiée. »* (renvoie vers A.III.)

#### A.II.8 — Métriques inter-moteurs

**A.II.8.a — Score de complémentarité quantifié.**

```
cer_oracle = tokens_GT_correctement_transcrits_par_AU_MOINS_un_moteur
             / total_tokens_GT
```

Borne inférieure atteignable par voting ensemble. Si `cer_oracle` est
très inférieur au meilleur moteur seul, ça justifie l'effort d'un
pipeline d'ensemble. Sinon non. Affiché à côté du meilleur CER observé
dans la synthèse factuelle.

**A.II.8.b — Score de spécialisation.** Pour chaque paire de moteurs,
la KL-divergence sur les distributions taxonomiques (déjà calculée en
A.II.1.c) sert de score de spécialisation. Moteurs spécialisés
différemment → candidats pour ensemble. Moteurs spécialisés
identiquement → pas de gain attendu.

#### A.II.9 — Métriques longitudinales

L'historique SQLite (`core/history.py`, Sprint 8) existe mais aucune
métrique n'en sort dans le rapport. C'est complémentaire du chantier
A.I.3.

**Pente de progression.** Régression linéaire sur les CER successifs
d'un moteur dans le temps. Trois nombres : pente, R², `n_runs`.
Nouvelle vue **« Évolution dans le temps »** du rapport, un graphe par
moteur avec zone de confiance bootstrap.

**Détection de point de rupture.** Algorithmes de change-point detection
(PELT, Bayesian via `ruptures` ou implémentation Python pure pour les
petits N) sur la série temporelle des CER. Identifie automatiquement la
version où un modèle a changé de comportement. Particulièrement utile
pour relier une régression à un changement de pipeline d'entraînement.

Détecteur narratif `regression_in_history` qui remonte *« Tesseract
montre une régression depuis le run du 12/02 (CER moyen +1,8 points sur
les 3 derniers benchmarks) »*.

### A.III — Stratification par `script_type` (vue transversale)

C'est probablement **la plus haute valeur ajoutée transversale** du plan
A. Aujourd'hui les détecteurs `stratum_winner` et `stratum_collapse`
(Sprint 19) calculent l'information par strate mais elle n'est pas
remontée dans le tableau de classement principal.

**Chantier.** Toggle dans la nav du rapport : *« Stratifier par
script_type »*. Quand activé, le tableau de classement se dédouble :

- Un sous-tableau par strate avec son propre top-3
- Ses propres tests statistiques (Wilcoxon, Friedman)
- Son propre Pareto

Le moteur narratif adapte sa synthèse :

> *« Sur les manuscrits gothiques (n=43), Pero domine ; sur les imprimés
> modernes (n=87), Tesseract suffit. Le classement global agrégé masque
> cette divergence. »*

**Architecture.** Le runner stocke déjà `script_type` par document
(metadata). L'agrégation actuelle est plate ; il faut introduire un
`StratifiedBenchmarkResults` qui contient une `BenchmarkResults` par
strate plus l'agrégat global. Le rapport HTML en mode stratifié remplace
les vues principales par des onglets de strates, avec un onglet « Tous »
qui reprend la vue actuelle.

**Contrainte UX.** Le toggle est désactivé automatiquement si une seule
strate est présente. Il est mis en évidence (badge `Recommandé`) si le
score d'homogénéité (A.II.7) est bas.

C'est le passage d'un rapport qui dit *« voici la performance moyenne »*
à un rapport qui dit *« voici quel moteur choisir pour quel type de
document »*.

**Effort A.III.** Un sprint complet, parce qu'il faut adapter toute la
chaîne de visualisation et plusieurs détecteurs narratifs.

---

## 4. Phase B — Banc d'essai de pipelines (mode optionnel)

Saut conceptuel qui répond à la question scientifique non-résolue
soulevée par le contexte BnF : **le classement des moteurs survit-il à
l'introduction d'une étape de reconstruction de structure ?**.

Ce mode reste **optionnel et masqué par défaut**. Un utilisateur du mode
benchmark texte simple ne voit jamais les vues B tant qu'il ne charge
pas une pipeline composée. Cf. § 1.3 (rapport adaptatif).

### B.1 — Acte 1 : premier cas BnF concret

**Pourquoi ne pas généraliser tout de suite.** L'erreur classique sur ce
type de projet : coder un framework avant d'avoir vu un cas d'usage
réel. On finit avec une abstraction élégante qui ne colle à aucun
workflow. Un cas BnF concret va révéler ce qui manque dans le modèle de
données, comment associer la GT par étape, quelles visualisations ont du
sens.

**Ce qu'il faut faire.** Choisir avec l'équipe BnF un corpus qui réunit
trois conditions :

- GT ALTO disponible et stable
- Volume modeste (50 à 200 pages) pour itérer vite
- Représentatif d'un cas réel (registres, presse, manuscrits)

Construire une première pipeline en YAML :

```yaml
pipeline:
  name: "pero_to_alto_v1"
  steps:
    - module: pero_ocr
      input: image
      output: text
    - module: alto_reconstructor_naive
      input: [image, text]
      output: alto

evaluation:
  junctions:
    - after: pero_ocr
      compare_to: gt.text
      metrics: [cer, wer]
    - after: alto_reconstructor_naive
      compare_to: gt.alto
      metrics: [reading_order_f1, layout_f1, text_preservation]
```

**Pas d'UI graphique à ce stade.** CLI uniquement :
`picarones pipeline run my-pipeline.yaml --corpus path/to/corpus`.

**Ce qui doit sortir.** Un rapport HTML qui montre, document par
document, ce que produit chaque étape de la pipeline et comment ça se
compare à la GT correspondante. Minimum viable.

**Critères de réussite.**

1. La pipeline tourne de bout en bout sur le corpus.
2. Au moins une métrique est calculée à chaque jonction.
3. On identifie au moins trois choses qui manquent dans le modèle de
   données ou dans l'interface module (entrée pour l'acte 2).
4. Le rapport est interprétable par un collègue BnF qui n'a pas codé la
   pipeline.

**Décision critique en fin d'acte 1.** Faire le bilan **avant** de passer
à l'acte 2. Si l'acte 1 a révélé des manques majeurs dans la Phase 0,
refactoriser maintenant. Si tout est solide, continuer.

**Effort B.1.** Deux à trois sprints — la marge couvre les découvertes.

### B.2 — Acte 2 : pipeline composée comme concept

À ce stade on a un cas qui marche. On peut généraliser.

**Extension du concept de « concurrent ».** Aujourd'hui un concurrent est
un moteur OCR ou un OCR+LLM. Demain c'est une chaîne de N modules. Le
runner traite uniformément les deux : un moteur OCR seul est une pipeline
à un seul nœud (cf. § 1.1).

**Métriques par jonction.** À chaque arête du DAG, calculer la métrique
adéquate selon les types d'artefacts. Le rapport décompose la performance
par étape :

> *« La pipeline A (Pero → reconstructor_v2) bat la pipeline B (Tesseract
> → reconstructor_v2) globalement (CER 4,2 % vs 7,8 %). Mais
> reconstructor_v2 produit un meilleur reading-order F1 quand il part de
> Tesseract (0,89 vs 0,82). La différence finale vient entièrement de la
> qualité OCR amont. »*

C'est cette analyse par étape qui transforme un benchmark en outil de
diagnostic.

**Synthèse factuelle adaptée.** Les détecteurs narratifs existants
gagnent des frères pour les pipelines :

- `pipeline_stage_collapse` — une étape effondre la performance
- `module_introduces_more_than_corrects` — voir B.3
- `pipeline_dominated_by_single_stage` — une étape porte tout le gain

Le moteur narratif compare des pipelines au lieu de comparer des moteurs
quand le mode pipeline est actif.

### B.3 — Acte 3 : métrique d'absorption d'erreur

**Pourquoi c'est critique.** Quand un module post-correction LLM aplatit
les différences entre OCR amont, ce n'est pas qu'il « améliore » tous
les moteurs — c'est qu'il introduit ses propres biais qui dominent ceux
de l'OCR. Mesurer la dégradation par étape ne suffit pas. Sans cette
métrique, on confond correction et écrasement, et la communauté
scientifique ne peut pas faire confiance aux conclusions.

**Ce qu'il faut faire.** À chaque jonction où un module transforme un
artefact, mesurer **deux flux séparément** :

- **Taux de correction** : parmi les erreurs présentes en entrée du
  module, combien sont corrigées en sortie ?
- **Taux d'introduction** : parmi les erreurs présentes en sortie,
  combien sont nouvelles (absentes en entrée) ?

C'est la généralisation du score de sur-normalisation existant
(chantier A.I.7) à toute jonction. La formule s'applique uniformément à
OCR→LLM, OCR→reconstructor, VLM→ALTO_mapper.

**Visualisation.** Un graphe **Sankey** par pipeline : à chaque jonction,
deux flux (corrections et introductions) avec leurs volumes. Permet de
voir au premier coup d'œil quels modules « ajoutent du bruit » sous
couvert d'amélioration globale.

**Détecteur narratif `module_writes_more_than_reads`.**

> *« Le module GPT-4o post-corrector introduit 1,3 erreur nouvelle pour
> chaque erreur corrigée — il écrit son propre texte plutôt que de
> corriger Tesseract. »*

C'est précisément la métrique qui légitimise une publication ICDAR/TPDL
sur le sujet.

### B.4 — Acte 4 : visualisation du DAG

**Outil d'inspection, pas de construction.** Le YAML reste source de
vérité.

**Ce qu'il faut faire.** Un nouveau module dans le rapport HTML :
**« Pipeline DAG »**. Affiche le graphe orienté de la pipeline. Chaque
nœud est un module. Chaque arête est annotée avec :

- Le type d'artefact transmis
- La métrique calculée à cette jonction (la plus pertinente)
- Une indication visuelle de qualité (vert/jaune/rouge selon seuils)

Clic sur une arête → drill-down par document : *« À cette étape,
document 47, voici ce qui sort du module en amont, voici ce qui sort en
aval, voici la GT correspondante. »*. Diff coloré façon GitHub à trois
colonnes.

**Pas de drag-and-drop, pas de notebook.** Le visuel sert à inspecter
et déboguer, pas à construire. Une institution sérieuse versionne ses
pipelines en YAML dans Git, pas en JSON exporté d'une UI.

### B.5 — Acte 5 : comparaison incrémentale

**Pourquoi c'est indispensable.** Avec 5 OCR × 3 reconstructeurs × 4
post-correcteurs × 3 mappeurs = 180 pipelines à comparer, le rapport
noie l'information. Il faut un mécanisme de comparaison contrôlée type
design d'expérience.

**Ce qu'il faut faire.** Une nouvelle vue **« Comparaison contrôlée »** :

- Fixer N−1 modules de la pipeline
- Faire varier le N-ième
- Voir l'effet isolé du module qui varie

Présentation : un tableau ANOVA-like qui isole l'effet du module variable
en contrôlant les autres. Tests statistiques associés (Friedman généralisé
sur les rangs des pipelines, Nemenyi pour les comparaisons par paires).

C'est presque un Latin square automatisé. Sans ça, le rapport sur 180
pipelines est inutilisable.

### B.6 — Acte 6 : politique de modules

**Avant d'ouvrir aux contributions externes.**

**Cadre de qualité pour les modules contribués.**

- Test unitaire obligatoire sur un corpus de référence (versionné dans
  le repo)
- Contrat d'entrée/sortie strict avec validation automatique au
  chargement
- Métadonnées obligatoires : licence, auteur, version, citation
  académique si applicable
- Score de qualité affiché dans le rapport pour chaque module utilisé

Un module qui ne passe pas l'audit n'est **pas exécutable**. Pas
exécutable = pas dans le rapport, pas dans la pipeline.

**Stratégie d'ouverture en deux temps.**

1. **Phase fermée.** Modules officiels uniquement, contributions via PR
   sur le repo principal. Tant que l'interface module n'est pas stable,
   accepter des contributions externes force à maintenir une compatibilité
   qu'on n'est pas prêt à offrir.
2. **Phase ouverte.** Une fois 5–6 modules officiels stables et un guide
   de contribution éprouvé, ouvrir via plugins (`picarones-module-X` sur
   PyPI avec mécanisme `entry_points`).

---

## 5. Trajectoire intégrée et calendrier

Le plan se déroule en **six étapes**, certaines en parallèle. Les axes A
et B partagent la même base de code et le même rapport — ils ne
s'opposent jamais.

### Étape 1 — Phase 0 dans son intégralité

**Durée.** 3 sprints (≈ 6 semaines).

Modèle de données multi-niveaux, interface module générique, métriques
composables. Non-négociable. **Aucune autre étape ne peut commencer tant
que ce n'est pas stable.**

À la fin : tous les tests existants passent, la rétrocompatibilité est
garantie, la base est posée pour les axes A et B.

### Étape 2 — Premier livrable de l'axe A

**Durée.** 1,5 sprint (≈ 3 semaines).

Trois métriques prioritaires (A.II.1 : NER, calibration, divergence
taxonomique) + **stratification par script_type (A.III)**, qui est
l'amélioration la plus visible pour les utilisateurs actuels.

Inclure aussi **A.I.2 (médiane par défaut)** parce qu'il s'agit d'un
changement à un seul endroit avec impact immédiat sur la lecture du
rapport.

À la fin : le rapport actuel a gagné une dimension d'utilité aval (NER),
une dimension de fiabilité (calibration), un classement plus honnête
(médiane), et une lecture stratifiée qui change la nature des
recommandations.

### Étape 3 — Acte 1 de l'axe B + suite de l'axe A en parallèle

**Durée.** 3 sprints (≈ 6 semaines).

- **Axe B** : cas BnF concret (B.1) — démarrage de la collaboration
  formalisée avec l'équipe BnF.
- **Axe A en parallèle** : critiques structurelles à fort ROI :
  - A.I.1 (worst lines + rare-token recall)
  - A.I.4 (taxonomie 3 niveaux)
  - A.I.9 (section « Leviers d'amélioration »)
  - A.II.2 (métriques structurelles, qui consommeront la GT ALTO du cas
    BnF)

Les deux axes ne se gênent pas : l'axe A travaille sur l'évaluation
texte+structure mono-pipeline, l'axe B sur l'évaluation pipeline
composée.

**Décision critique en fin d'étape 3.** Bilan du cas BnF avant de passer
à l'étape suivante. Refactoriser la Phase 0 si besoin.

### Étape 4 — Actes 2 et 3 de l'axe B + suite de l'axe A

**Durée.** 3,5 sprints (≈ 7 semaines).

- **Axe B** : pipeline composée comme concept (B.2) + métrique
  d'absorption d'erreur (B.3). C'est ici que la dimension publication
  ICDAR/TPDL devient réelle.
- **Axe A en parallèle** :
  - A.I.3 (vue « inhabituel ici » + détecteur `engine_off_baseline`)
  - A.I.5 (curseur normalisation diplomatique)
  - A.I.6 (volume cible projeté)
  - A.II.4 (fiabilité : IAA + stabilité multi-runs)

À la fin de l'étape 4, **un papier scientifique est rédactible** sur les
résultats du cas BnF avec absorption d'erreur quantifiée.

### Étape 5 — Visualisation DAG + métriques longitudinales et avancées

**Durée.** 3 sprints (≈ 6 semaines).

- **Axe B** : visualisation DAG (B.4)
- **Axe A** :
  - A.I.7 (sur-normalisation LLM en vue analytique)
  - A.I.8 (robustesse projetée)
  - A.II.3 (philologiques : Unicode/abbrev/MUFI)
  - A.II.5 (utilisabilité aval)
  - A.II.6 (économique)
  - A.II.7 (image prédictives)
  - A.II.8 (inter-moteurs : oracle + spécialisation)
  - A.II.9 (longitudinales)

À ce stade, l'outil est mature et commence à attirer l'attention de la
communauté patrimoniale.

### Étape 6 — Maturité institutionnelle

**Durée.** 3 sprints (≈ 6 semaines).

- B.5 (comparaison incrémentale)
- B.6 (politique de modules)
- Ouverture aux contributions externes en phase fermée → ouverte
- Documentation institutionnelle (guide d'écriture de modules,
  audit-bench)
- Premiers retours d'autres institutions

### Synthèse temporelle

| Étape | Durée | Cumul | Livrable saillant |
|---|---:|---:|---|
| 1 — Phase 0 | 3 sprints | 6 sem | Fondations multi-niveaux |
| 2 — A premier livrable | 1,5 sprint | 9 sem | NER + calibration + médiane + stratif |
| 3 — B acte 1 + A suite | 3 sprints | 15 sem | Premier cas BnF + worst lines + leviers |
| 4 — B actes 2-3 + A | 3,5 sprints | 22 sem | Absorption d'erreur (publication possible) |
| 5 — DAG + métriques restantes | 3 sprints | 28 sem | Outil mature |
| 6 — Maturité | 3 sprints | 34 sem | Ouverture externe |

**Total estimé.** 17 sprints, soit **8–9 mois** à un sprint de deux
semaines. Ambitieux mais cohérent avec ce qui a déjà été construit
(22 sprints terminés à date).

---

## 6. Décisions structurantes à trancher avant le premier sprint

Trois choix qui contraignent tout le reste. Mieux vaut les fixer
maintenant que les découvrir au sprint 8.

### 6.1 Périmètre de la marque « Picarones »

**Question.** Dans le mode legacy, Picarones reste un benchmark. Dans le
mode pipeline, Picarones devient un banc d'essai. Faut-il un seul
produit ou deux noms ?

**Recommandation.** **Un seul nom.** Les deux dimensions partagent la
même philosophie (rigueur, traçabilité, absence de prescription) et le
même socle technique. Documenter clairement les deux modes d'usage dans
le `README.md` mis à jour, avec une section *« À qui ça s'adresse »* qui
distingue les deux profils.

### 6.2 Partenariat BnF formalisé

**Question.** Le plan B est intenable sans un cas BnF en condition
réelle.

**Recommandation.** Co-écrire dès maintenant un document court (1 page)
avec l'équipe BnF qui définit :

- Quel corpus (référence stable, accord d'utilisation)
- Quelle GT disponible et à quel niveau (texte ? ALTO ? les deux ?)
- Quelle pipeline cible (que veut-on évaluer ?)
- Quel critère de succès (que doit produire le rapport ?)
- Quel calendrier (qui livre quoi à quelle date ?)

Sans ce document, on construit sur des hypothèses internes qui peuvent
diverger des besoins réels.

### 6.3 Politique de contribution externe

**Question.** Accepter ou non des modules tiers dès le départ ?

**Recommandation.** **Commencer fermé**, pour deux raisons :

1. Tant que l'interface module n'est pas stable, accepter des
   contributions externes force à maintenir une compatibilité qu'on
   n'est pas prêt à offrir.
2. Avant 5–6 modules officiels stables et bien testés, on n'a pas le
   recul pour juger de la qualité d'un module externe.

Ouvrir au sprint 17 (étape 6), pas avant.

---

## 7. Le cap à tenir

Le **différenciateur de fond** reste celui qui distingue déjà Picarones :
**rigueur méthodologique, traçabilité, absence de prescription**. Tout ce
qui s'ajoute doit servir cette ligne, pas la diluer.

### 7.1 Pièges identifiés à ne pas commettre

**Piège 1 — Coder le framework de l'axe B avant le cas d'usage.** La
Phase 0 prépare l'infrastructure, mais l'acte 1 de l'axe B doit
absolument commencer par un cas BnF concret avant toute généralisation.
Si on code `BaseModule`, le runner pipeline et la visualisation DAG en
partant d'intuitions, on finira avec une abstraction qui ne colle à
aucun workflow.

**Piège 2 — Le mode hybride en patchwork.** Tentation : ajouter des
options à `picarones run` au fil du temps (`--with-pipeline`,
`--enable-junction-metrics`, `--show-dag`). Au bout de six mois, quinze
flags qui interagissent mal. Mauvaise voie. La bonne voie : un seul
concept central — la pipeline — qui peut avoir un seul nœud (cas legacy
invisible) ou plusieurs (cas avancé explicite). La CLI legacy
`picarones run` construit en interne une pipeline triviale.
L'utilisateur ne voit jamais cette construction.

**Piège 3 — La progressivité comme concession.** Tentation inverse :
afficher tout dans le rapport, « au cas où l'utilisateur en aurait
besoin ». Si une vue n'apporte rien quand la pipeline est simple, elle
doit être **absente** — pas juste vide ou désactivée. La discipline du
masquage automatique est ce qui distingue un bon produit d'un produit
qui veut tout faire.

**Piège 4 — Aplatir les jonctions.** Une métrique calculée à une jonction
text→text (CER) et une métrique à une jonction image→ALTO (Layout F1) ne
sont pas comparables. Le rapport doit clairement séparer les deux dans
les vues. Le typage strict de la Phase 0.3 garantit cette séparation,
**à condition de ne jamais contourner le système**.

**Piège 5 — Régressions silencieuses.** À chaque sprint, garder un set
de tests qui exécute des configurations purement legacy (pas de YAML,
pas de pipeline composée, GT mono-niveau) et vérifie que le rapport
produit est strictement identique octet par octet à ce qui est produit
aujourd'hui. C'est le seul garde-fou contre les régressions silencieuses
quand on enrichit le runner.

### 7.2 Question à se poser à chaque PR

À chaque décision de design, à chaque PR, une seule question :

> *« Est-ce que ce changement oblige l'utilisateur du mode simple à
> apprendre quelque chose de nouveau ? »*

Si oui, **mauvais design**. Repenser jusqu'à ce que la réponse soit non.

C'est la règle qui garantit que les axes A et B coexistent vraiment au
lieu de cohabiter difficilement.

---

## 8. Annexe — Mapping critiques → chantiers

Vérification que chaque point soulevé dans la conversation de cadrage
trouve sa réponse dans ce plan.

### 8.1 Critiques structurelles

| # | Critique | Chantier(s) |
|---:|---|---|
| 1 | Granularité s'arrête à la page | A.I.1 (worst lines + rare-token recall) |
| 2 | CER moyen au lieu de médian | A.I.2 (médiane par défaut) |
| 3 | Pas de vue « inhabituel ici » | A.I.3 (encart historique + détecteur off-baseline) |
| 4 | Taxonomie sous-exploitée | A.I.4 (co-occurrence + intra-doc + comparative) |
| 5 | Normalisation diplomatique binaire | A.I.5 (curseur dans panneau Avancé) |
| 6 | Coût décorrélé de la valeur | A.I.6 (volume cible projeté) + A.II.6 (coût marginal) |
| 7 | Sur-normalisation LLM en colonne | A.I.7 (vue analytique + table fréquences) |
| 8 | Robustesse déconnectée du benchmark | A.I.8 (projection qualité réelle sur courbe) |
| 9 | Pas de signal sur leviers | A.I.9 (section dédiée + registre `levers/`) |

### 8.2 Métriques additionnelles

| Métrique | Chantier |
|---|---|
| NER (précision/rappel/F1 par catégorie) | A.II.1.a |
| Calibration des moteurs (ECE, MCE, reliability) | A.II.1.b |
| Divergence taxonomique inter-moteurs (KL/JS) | A.II.1.c |
| Reading order F1 (ICDAR 2015) | A.II.2 |
| Layout F1 par type de région | A.II.2 |
| Différence Flesch GT vs OCR | A.II.2 |
| Précision par bloc Unicode | A.II.3 |
| Score d'expansion d'abréviations (strict/expansion) | A.II.3 |
| Couverture MUFI | A.II.3 |
| Inter-annotator agreement (Cohen κ / Krippendorff α) | A.II.4 |
| Stabilité multi-runs | A.II.4 |
| Recherchabilité (Levenshtein ≤ 2) | A.II.5 |
| Précision sur séquences numériques | A.II.5 |
| Throughput effectif | A.II.6 |
| Coût marginal par erreur évitée | A.II.6 |
| Complexité paléographique | A.II.7 |
| Homogénéité du corpus | A.II.7 |
| CER oracle / complémentarité | A.II.8.a |
| Score de spécialisation (KL) | A.II.8.b |
| Pente de progression (régression linéaire) | A.II.9 |
| Détection de point de rupture (PELT/Bayesian) | A.II.9 |

### 8.3 Banc d'essai pipelines (contexte BnF)

| Question | Acte |
|---|---|
| GT multi-niveaux (texte + ALTO + entités) | Phase 0.1 |
| Module générique typé I/O | Phase 0.2 |
| Métriques par jonction | Phase 0.3 |
| Premier cas BnF (Pero → reconstructeur ALTO) | B.1 |
| Pipeline composée comme concurrent | B.2 |
| Absorption vs introduction d'erreur | B.3 |
| Visualisation DAG inspectable | B.4 |
| Comparaison contrôlée (Latin square) | B.5 |
| Politique de modules contribués | B.6 |

### 8.4 Synthèse de l'unification produit

| Préoccupation soulevée | Réponse architecturale |
|---|---|
| Coexister sans gêner les chercheurs actuels | § 1.1 (pipeline 1-nœud = cas dégénéré) |
| Trois modes d'entrée sans complexifier la CLI | § 1.2 (legacy / YAML / hybride) |
| Vues B masquées en mode simple | § 1.3 (rapport adaptatif) |
| Pas obliger à apprendre du nouveau | § 1.4 + § 7.2 (règle pratique) |
| Une seule base de code, pas deux produits | § 6.1 (un seul nom Picarones) |

---

## 9. Pour démarrer

Ordre concret des trois prochaines actions, dans cet ordre, sans
négociation :

1. **Co-écrire le mémo BnF (§ 6.2)** — 1 page, signée des deux côtés,
   définissant corpus / GT / pipeline cible / critère / calendrier.
2. **Ouvrir une branche `phase-0/multi-level-gt`** et coder la refonte
   du modèle `Document` (chantier 2.1) en gardant la rétrocompatibilité
   stricte (test legacy bit-à-bit dès le premier commit).
3. **Lister les modules officiels candidats pour la phase fermée
   (§ 6.3)** : OCR existants, premier reconstructeur ALTO BnF, premier
   post-correcteur LLM. Cette liste devient le périmètre de stabilisation
   de l'interface module pour les sprints 1 à 17.

Tout le reste découle.