# 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 ```python 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 : ```yaml 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` ```yaml 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` : ```javascript > DATA.snapshots.environment.git_commit "17cc5474abc..." ``` Puis : ```bash 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 : ```bash 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 : ```bash 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 ```bash 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 : ```bibtex @misc{picarones_2026, title = {Picarones: Heritage OCR/HTR/VLM Benchmarking Platform}, author = {}, year = {2026}, doi = {10.5281/zenodo.}, 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.