Spaces:
Sleeping
É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
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 imageTEXT— chaîne de caractères (transcription)ALTO/PAGE— XML structurel (objetsAltoGT/PageGT)ENTITIES— liste d'entités nomméesREADING_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
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)
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
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)
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)
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 :
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)
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 :
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)
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)
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
TEXTdoit accepter une chaîne, pas unTextGT. 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→strENTITIES→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 dansoutput_types. Le runner validera automatiquement et évaluera contre laAltoGTsi 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/core/runner.py) — 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 |