Picarones / docs /reference /reproducibility-snapshots.md
Claude
chore(versioning): reposition project as 0.9.0 (pre-1.0)
416bee1 unverified

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 :

  1. Le rapport HTML autonome (qui contient tout le snapshot).
  2. requirements-dev.lock du moment du benchmark (pour ré-instancier l'environnement).
  3. 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.