Spaces:
Sleeping
Snapshots de reproductibilité
Pourquoi des snapshots ?
Pour qu'un benchmark Picarones soit citable scientifiquement, un lecteur doit pouvoir, des années plus tard, comprendre exactement ce qui a été mesuré. Un rapport HTML qui dit « Tesseract 5.3.4 obtient un CER de 4,2 % sur ce corpus » est inutilisable s'il n'indique pas :
- la table de pricing utilisée (qui a évolué entre temps),
- la version exacte des prompts appliqués aux pipelines OCR+LLM,
- le profil de normalisation effectivement appliqué (avec ses équivalences diplomatiques),
- le commit Picarones utilisé pour produire le rapport,
- les paquets Python installés au moment du run.
Le module picarones.reports.html.snapshot agrège ces cinq dimensions et
les embarque dans le JSON du rapport, sous la clé
report_data["snapshots"]. Le rapport HTML reste auto-portant : un
lecteur peut tout retrouver sans accès au repo source.
Ce qu'un snapshot contient
from picarones.reports.html.snapshot import snapshot_all
snap = snapshot_all(
lang="fr",
normalization_profile=profile, # Profile dataclass ou None
)
Retourne un dict avec quatre clés top-level :
| Clé | Contenu | Source |
|---|---|---|
pricing |
YAML brut intégral de picarones/data/pricing.yaml |
pricing_snapshot() |
glossary |
Entrées du glossaire effectivement référencées dans la synthèse (langue rendue) | glossary_snapshot() |
normalization |
Profil sérialisé (diplomatic_table, exclude_chars, drapeaux NFC/case-folding…) |
normalization_snapshot() |
environment |
Version Picarones, Python, plateforme OS, commit git, paquets installés (top 200) | environment_snapshot() |
pricing
Le YAML est embarqué verbatim. Si le tarif d'OpenAI change demain, le rapport d'aujourd'hui reste lisible — l'analyse Pareto coût garde sa valeur historique car elle pointe vers la table effectivement utilisée.
glossary
Pas le glossaire complet (~25 entrées) — seulement celles qui sont effectivement référencées par la synthèse narrative ou les vues ouvertes au moment du snapshot. Économie de poids ; un ancien rapport reste documenté même si le glossaire évolue.
normalization
Le profil contient :
name: medieval_french
nfc: true
casefold: false
diplomatic_table:
ſ: s
u: v
i: j
ꝑ: per
⁊: et
exclude_chars:
- "·"
- "¶"
Permet à un relecteur de comprendre exactement quelles équivalences ont été appliquées au moment du calcul de CER, sans relancer.
environment
picarones_version: "0.9.0"
python_version: "3.11.13"
platform: "Linux 6.18.5-x86_64-with-glibc2.39"
git_commit: "17cc5474..."
installed_packages:
- "click==8.3.3"
- "defusedxml==0.7.1"
- "jiwer==3.1.0"
# ... top 200 trié alpha
Le git_commit est lu via git rev-parse HEAD (subprocess timeout 2 s,
sans shell). Si le repo n'est pas un dépôt git, la clé reste None.
Comment rejouer un benchmark à 5 ans d'écart
Étape 1 — Récupérer le commit Picarones d'origine
Dans le rapport HTML, ouvrir la console JS et inspecter DATA.snapshots.environment.git_commit :
> DATA.snapshots.environment.git_commit
"17cc5474abc..."
Puis :
git clone https://github.com/maribakulj/Picarones.git
cd Picarones
git checkout 17cc5474abc
Étape 2 — Récréer l'environnement Python
Picarones livre des lock files :
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.lock
Si vous avez besoin d'une version Python différente (par exemple un ancien rapport rendu en Python 3.11.10), utiliser pyenv :
pyenv install 3.11.10
pyenv local 3.11.10
Étape 3 — Récréer le corpus + GT
Picarones ne stocke pas les images du corpus dans le snapshot (les images appartiennent au déposant). Il faut donc re-récupérer le corpus original. Pour les imports IIIF, le manifeste est durable (les bibliothèques nationales versionnent) ; pour les uploads ZIP, l'utilisateur doit conserver son archive source.
Si le corpus a déjà été chargé dans Gallica / HTR-United, le
metadata.source_url de chaque Document permet le re-fetch.
Étape 4 — Rejouer le benchmark
picarones run \
--corpus ./corpus_recovered/ \
--engines tesseract,pero_ocr \
--output rerun.json \
--normalization medieval_french
Étape 5 — Vérifier la concordance
Le commit + le lock file + le profil de normalisation garantissent que les métriques CER / WER / MER / WIL seront bit-à-bit identiques.
Différences possibles légitimes :
- L'OCR cloud (Mistral / Google / Azure) peut avoir évolué côté serveur — les chiffres peuvent diverger même avec un client identique. Pour un benchmark scientifiquement reproductible, privilégier Tesseract / Pero OCR (modèles versionnés et locaux).
- Les LLMs évoluent constamment ; un pipeline OCR+LLM rejoué 6 mois plus tard peut donner d'autres résultats. Le snapshot des prompts reste utile mais ne reproduit pas le LLM lui-même.
Snapshot et publication scientifique
Pour un papier scientifique, citer Picarones doit indiquer :
@misc{picarones_2026,
title = {Picarones: Heritage OCR/HTR/VLM Benchmarking Platform},
author = {<auteurs>},
year = {2026},
doi = {10.5281/zenodo.<id>},
version= {1.1.0},
url = {https://github.com/maribakulj/Picarones}
}
Et dans le matériel supplémentaire, joindre :
- Le rapport HTML autonome (qui contient tout le snapshot).
requirements-dev.lockdu moment du benchmark (pour ré-instancier l'environnement).- Le digest Docker si vous avez utilisé l'image officielle :
docker inspect ghcr.io/maribakulj/picarones:1.1.0 --format='{{.Id}}'.
Un évaluateur scientifique disposera ainsi des éléments matériels pour vérifier l'analyse — c'est l'exigence minimale d'une publication reproductible (cf. Stodden et al., Computational reproducibility).
Limites assumées
- Le code source n'est pas embarqué dans le snapshot. On embarque l'identifiant du commit, pas le diff. Si le repo est rendu privé ou supprimé, le snapshot devient orphelin. Mitigation : publier les versions sur Zenodo (DOI durable, garanti 20 ans).
- Les images du corpus ne sont pas snapshottées (poids + droits d'auteur). Le déposant doit conserver son corpus.
- Le LLM cloud ne peut pas être snapshotté — c'est une dépendance externe non reproductible. Pour la science, préférer les modèles ouverts (Llama, Phi, Mistral via Ollama).
Tests
tests/report/test_reproducibility_snapshots.py
valide que snapshot_all() est :
- déterministe (même input → même bytes en sortie),
- complet (toutes les clés top-level présentes),
- robuste (ne crashe pas si git absent, si pricing.yaml manquant…).
tests/test_reproducibility_ops.py ajoute la validation
de la chaîne lock file + Docker digest + snapshot comme contrat
opérationnel.