File size: 13,506 Bytes
e8b4408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979f3c3
e8b4408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979f3c3
e8b4408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979f3c3
e8b4408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979f3c3
e8b4408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b09377
e8b4408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# Écrire un module pour le banc d'essai de pipelines

> **Public visé** : chercheurs, ingénieurs, équipes patrimoniales
> qui veulent **évaluer leurs propres modules** (correcteur LLM,
> reconstructeur ALTO, classifieur d'entités, re-segmenteur…)
> dans Picarones.

> **Ce que Picarones est et ce qu'il n'est pas**
>
> Picarones est un **banc d'essai**, pas un atelier de production.
> Il fournit l'infrastructure pour exécuter, mesurer et comparer
> vos modules sur un corpus avec sa GT — il ne fournit **aucun
> module métier** (pas de reconstructeur ALTO « maison », pas de
> correcteur LLM intégré, pas de re-segmenteur).  C'est à vous
> d'amener vos modules ; Picarones les juge.

## TL;DR

```python
from picarones.core.modules import BaseModule, ArtifactType
from picarones.core.pipeline import (
    PipelineRunner, PipelineSpec, PipelineStep,
)

class MyCorrector(BaseModule):
    input_types = (ArtifactType.TEXT,)
    output_types = (ArtifactType.TEXT,)
    execution_mode = "io"          # ou "cpu"

    @property
    def name(self) -> str:
        return "my-corrector-v1"

    def process(self, inputs):
        text = inputs[ArtifactType.TEXT]
        # … votre logique : appel LLM, regex, modèle local, etc.
        return {ArtifactType.TEXT: text.replace("teh", "the")}

spec = PipelineSpec(
    name="ocr_then_corrector",
    steps=[
        PipelineStep("ocr",  my_ocr_module),       # votre module OCR
        PipelineStep("fix",  MyCorrector()),
    ],
)
result = PipelineRunner.run(
    spec, document, {ArtifactType.IMAGE: "/path/to/img.png"},
)
print(result.junction_metrics_for(ArtifactType.TEXT))
# → {"cer": 0.05, "wer": 0.12, ...}
```

## 1. Le contrat `BaseModule`

Un module Picarones est une classe qui hérite de `BaseModule` et
déclare quatre choses :

| Champ            | Rôle                                                   | Exemple                                |
| ---------------- | ------------------------------------------------------ | -------------------------------------- |
| `input_types`    | Tuple des types d'artefacts consommés                  | `(ArtifactType.TEXT,)`                 |
| `output_types`   | Tuple des types d'artefacts produits                   | `(ArtifactType.TEXT,)`                 |
| `execution_mode` | `"io"` (réseau, disque) ou `"cpu"` (calcul intensif)   | `"io"` pour un appel LLM cloud         |
| `name`           | Identifiant lisible pour le rapport et les logs        | `"my-corrector-v1"`                    |
| `process`        | La méthode qui transforme un dict d'inputs en outputs  | voir TL;DR                             |

Les `ArtifactType` disponibles aujourd'hui :

- `IMAGE` — typiquement chemin vers le fichier image
- `TEXT` — chaîne de caractères (transcription)
- `ALTO` / `PAGE` — XML structurel (objets `AltoGT` / `PageGT`)
- `ENTITIES` — liste d'entités nommées
- `READING_ORDER` — ordre de lecture des régions

## 2. Exemples pédagogiques (à NE PAS copier en production)

> Ces exemples sont **mockés** — leur seul rôle est d'illustrer
> le contrat.  Pour évaluer un vrai module, vous écrirez votre
> propre classe qui appelle votre vraie logique.

### 2.a Correcteur LLM TEXT → TEXT

```python
class LLMCorrector(BaseModule):
    """Mock pédagogique d'un correcteur LLM."""
    input_types = (ArtifactType.TEXT,)
    output_types = (ArtifactType.TEXT,)
    execution_mode = "io"

    def __init__(self, model: str) -> None:
        self._model = model

    @property
    def name(self) -> str:
        return f"llm-correcteur-{self._model}"

    def process(self, inputs):
        text = inputs[ArtifactType.TEXT]
        # Production : appel à votre LLM ici (OpenAI, Mistral, …)
        # corrected = my_llm_client.correct(text, model=self._model)
        corrected = text  # mock pédagogique : passthrough
        return {ArtifactType.TEXT: corrected}
```

### 2.b Reconstructeur TEXT → ALTO (mock)

```python
class TextToAltoReconstructor(BaseModule):
    """Mock pédagogique d'un reconstructeur ALTO depuis du texte.

    En production : votre code reconstruit la structure XML ALTO
    en plaçant chaque mot dans une boîte selon une heuristique
    (longueur de mot, hauteur de ligne, …).
    """
    input_types = (ArtifactType.TEXT,)
    output_types = (ArtifactType.ALTO,)
    execution_mode = "cpu"

    @property
    def name(self) -> str:
        return "text-to-alto-mock"

    def process(self, inputs):
        text = inputs[ArtifactType.TEXT]
        # Production : votre logique de reconstruction
        alto_payload = my_reconstruct(text)
        return {ArtifactType.ALTO: alto_payload}
```

### 2.c Classifieur TEXT → ENTITIES

```python
class NERExtractor(BaseModule):
    input_types = (ArtifactType.TEXT,)
    output_types = (ArtifactType.ENTITIES,)
    execution_mode = "cpu"

    @property
    def name(self) -> str:
        return "ner-extractor-spacy-fr"

    def process(self, inputs):
        text = inputs[ArtifactType.TEXT]
        # Production : votre extracteur (spaCy, HuggingFace, HIPE, …)
        entities = my_ner(text)  # liste de dicts {label, start, end, text}
        return {ArtifactType.ENTITIES: entities}
```

## 3. Orchestrer une pipeline

### 3.a Mono-document (Sprint 63)

```python
from picarones.core.pipeline import (
    PipelineRunner, PipelineSpec, PipelineStep,
)

spec = PipelineSpec(
    name="ocr_then_correct",
    steps=[
        PipelineStep("ocr",     my_ocr_module),
        PipelineStep("correct", LLMCorrector(model="my-model")),
    ],
)
result = PipelineRunner.run(
    spec, document, {ArtifactType.IMAGE: document.image_path},
)
print(result.succeeded)                                   # True / False
print(result.junction_metrics_for(ArtifactType.TEXT))     # CER, WER…
print(result.failing_steps)                               # noms des étapes en erreur
```

À chaque sortie d'étape, Picarones évalue **automatiquement**
l'artefact contre la GT du même niveau (via `compute_at_junction`,
Sprint 34).  Vous n'avez rien à câbler explicitement — il suffit
que `Document.ground_truths` porte une `TextGT` (ou `AltoGT`,
`EntitiesGT`…) au niveau correspondant.

### 3.b Corpus complet (Sprint 64)

```python
from picarones.measurements.pipeline_benchmark import run_pipeline_benchmark

bench = run_pipeline_benchmark(spec, my_corpus)
print(bench.n_pipelines_succeeded, "/", bench.n_docs)

agg = bench.aggregate_for_step("correct")
print(agg.duration_seconds_mean)                          # durée moyenne
print(agg.junction_metrics["text"]["cer"]["median"])      # CER médian
print(agg.error_breakdown)                                # types d'erreur
```

Pour les pipelines qui ne démarrent pas par `IMAGE` (par exemple
un re-segmenteur ALTO qui démarre depuis un ALTO pré-existant),
vous fournissez votre propre factory :

```python
def my_factory(doc):
    return {ArtifactType.ALTO: my_load_alto(doc)}

bench = run_pipeline_benchmark(spec, corpus, initial_inputs_factory=my_factory)
```

### 3.c Comparer N pipelines (Sprint 65)

```python
from picarones.measurements.pipeline_comparison import compare_pipelines

comparison = compare_pipelines(
    [spec_baseline, spec_with_correcteur_a, spec_with_correcteur_b],
    corpus,
)

# Classement par CER (plus bas = meilleur)
for name, cer in comparison.ranking_by_final_metric(
    ArtifactType.TEXT, "cer",
):
    print(f"{name}: {cer:.4f}" if cer else f"{name}: N/A")

# Gain vs baseline
gains = comparison.gain_table(
    ArtifactType.TEXT, "cer", baseline_pipeline="baseline",
)
```

### 3.d DAG branchant via `inputs_from` (Sprint 66)

Quand plusieurs étapes produisent le même type, la sortie de la
plus récente écrase les précédentes par défaut.  Pour comparer
deux corrections d'un même OCR dans une **seule** pipeline, vous
désignez explicitement l'étape source :

```python
spec = PipelineSpec(
    name="fork",
    steps=[
        PipelineStep("ocr", MyOCR()),
        PipelineStep(
            "correct_a", CorrecteurA(),
            inputs_from={ArtifactType.TEXT: "ocr"},     # depuis OCR
        ),
        PipelineStep(
            "correct_b", CorrecteurB(),
            inputs_from={ArtifactType.TEXT: "ocr"},     # depuis OCR aussi
        ),
    ],
)
```

Sans `inputs_from`, `correct_b` aurait reçu la sortie de
`correct_a` (chaîne).

> Pour comparer **plusieurs pipelines distinctes** apple-to-apple,
> préférez `compare_pipelines` (Sprint 65) — c'est plus clair et
> ça produit un rapport HTML dédié (Sprint 68).

## 4. Générer un rapport HTML autonome

### 4.a Pipeline unique (Sprint 67)

```python
from pathlib import Path
from picarones.report.pipeline_render import build_pipeline_report_html

bench = run_pipeline_benchmark(spec, corpus)
Path("rapport_pipeline.html").write_text(
    build_pipeline_report_html(bench, lang="fr"),
)
```

### 4.b Comparaison de N pipelines (Sprint 68)

```python
from picarones.core.modules import ArtifactType
from picarones.report.pipeline_render import (
    RankingSpec, build_pipeline_comparison_report_html,
)

html = build_pipeline_comparison_report_html(
    comparison,
    ranking_specs=[
        RankingSpec(ArtifactType.TEXT, "cer", label="CER"),
        RankingSpec(ArtifactType.TEXT, "wer", label="WER"),
    ],
    baseline_pipeline="baseline",
    lang="fr",
)
Path("comparaison.html").write_text(html)
```

L'utilisateur déclare **explicitement** ce qu'il veut voir : les
classements (`ranking_specs`) et la baseline éventuelle.  Pas
d'auto-détection magique — vous pilotez.

## 5. Bonnes pratiques

### 5.a Discipline des types

- Un module qui consomme `TEXT` doit accepter une chaîne, pas un
  `TextGT`.  Picarones extrait automatiquement le payload d'une
  GT typée avant de l'évaluer ; mais à l'intérieur d'un module,
  on travaille avec les types « bruts » :
  - `TEXT``str`
  - `ENTITIES``list[dict]`
  - `ALTO` / `PAGE` → objet structuré (à vous de choisir le
    schéma — Picarones n'impose pas)
- Si votre module produit un type différent de ses inputs (par
  ex. `TEXT` → `ALTO`), déclarez-le dans `output_types`.  Le runner
  validera automatiquement et évaluera contre la `AltoGT` si elle
  existe.

### 5.b Erreurs gracieuses

Un module qui lève une exception **n'arrête pas** la pipeline :
le runner capture l'exception, marque l'étape en erreur, et
continue avec les étapes suivantes (qui rapporteront « entrée
manquante » si elles dépendaient de la sortie de l'étape échouée).

Vous n'avez pas besoin de capturer vos propres exceptions — laissez
Picarones le faire pour bénéficier de la trace dans
`StepResult.error`.

### 5.c Mesure du temps

Picarones chronomètre **wall-clock** chaque appel `process`.  Si
votre module fait du caching interne ou du batching, c'est sa
durée réelle vue par l'utilisateur qui est mesurée — c'est ce
qu'on veut pour comparer fairement.

### 5.d Pas de seuils éditoriaux dans votre module

Si votre module classe en interne (ex. « ce texte semble
diplomatique »), ne reportez pas ce verdict dans Picarones — c'est
au chercheur qui lit le rapport de juger selon ses critères
éditoriaux.  Votre module produit un artefact, Picarones mesure
l'écart à la GT, le chercheur conclut.

## 6. Anti-patterns

### 6.a « Et si Picarones avait un correcteur LLM intégré ? »

Non.  Picarones est un **banc d'essai** : si on intégrait un
correcteur, on devrait le maintenir, le faire évoluer, le calibrer,
le documenter — et au final on **biaiserait** les benchmarks (parce
qu'on connaîtrait mieux notre correcteur que les autres).

À la place, vous écrivez votre `BaseModule` qui wrappe le
correcteur que vous voulez évaluer.  Picarones se contente de le
brancher dans la pipeline et de mesurer.

### 6.b « Et si je veux juste tester une pipeline OCR seule, sans étapes en aval ? »

C'est exactement ce que fait le runner OCR historique
(`run_benchmark` dans `picarones/measurements/runner/`) — il est
toujours là, n'a pas changé, et reste la voie recommandée pour
les benchmarks d'OCR mono-étage.

L'axe B (pipelines composées) sert quand vous avez **plusieurs
modules tiers** à enchaîner ou à comparer.

### 6.c « Mon module a besoin d'un état mutable entre documents »

Possible mais à vos risques.  Les `BaseModule` sont instanciés
une fois et leur méthode `process` est appelée pour chaque
document du corpus.  Si vous gardez de l'état dans `self`, il
persistera — utile pour un cache, dangereux si vous ne savez pas
ce que vous faites.

Pour la parallélisation future (à arbitrer dans un sprint dédié),
mieux vaut concevoir vos modules **stateless**.

## 7. Référence rapide des sprints axe B

| Sprint | Sujet                                            |
| ------ | ------------------------------------------------ |
| 32     | GT multi-niveaux (`Document.ground_truths`)      |
| 33     | Interface `BaseModule` + `ArtifactType`          |
| 34     | Registre typé de métriques (`compute_at_junction`) |
| 63     | `PipelineRunner` mono-document                   |
| 64     | `run_pipeline_benchmark` corpus-wide             |
| 65     | `compare_pipelines` apple-to-apple               |
| 66     | DAG branchant via `inputs_from`                  |
| 67     | `build_pipeline_report_html` autonome            |
| 68     | `build_pipeline_comparison_report_html`          |
| 69     | Ce guide                                         |