# CLAUDE.md — Picarones Plateforme de benchmark OCR/HTR pour documents patrimoniaux. Repo : github.com/maribakulj/Picarones HuggingFace Space : huggingface.co/spaces/Ma-Ri-Ba-Ku/Picarones (Docker, port 7860) --- ## Architecture en 3 cercles Voir le manifeste complet dans [`docs/architecture.md`](docs/architecture.md). ``` Cercle 3 (extras, report, cli, web) │ ▼ Cercle 2 (measurements, engines, llm, pipelines, modules) │ ▼ Cercle 1 (core) ``` Règle de dépendance stricte : les imports vont uniquement de l'extérieur vers l'intérieur. **Aucun shim** — un module a un seul emplacement. --- ## Setup ```bash pip install -e ".[dev,web]" # toujours inclure [web] pour les tests pytest tests/ -q --tb=short # lancer les tests picarones demo --output rapport.html # rapport démo sans moteur installé picarones serve --port 8080 # interface web locale ``` --- ## Structure ``` picarones/ ├── core/ Cercle 1 — abstractions pures (7 modules) │ ├── modules.py BaseModule, ArtifactType │ ├── corpus.py Document, Corpus, GTLevel, payloads typés │ ├── results.py DocumentResult, EngineReport, BenchmarkResult │ ├── metric_registry.py MetricSpec, register_metric, compute_at_junction │ ├── metric_hooks.py register_document_metric, register_corpus_aggregator │ ├── pipeline.py PipelineRunner, PipelineSpec, PipelineStep │ └── facts.py Fact, FactType, FactImportance, DetectorRegistry │ ├── measurements/ Cercle 2 — métriques officielles (~55 modules) │ ├── runner.py run_benchmark (orchestration) │ ├── statistics/ sous-package (Wilcoxon, Friedman/Nemenyi, bootstrap, Pareto, clustering, corrélation, distributions, CDD) │ ├── metrics.py / normalization.py / builtin_hooks.py │ ├── confusion.py / taxonomy.py / calibration.py / line_metrics.py / ... │ ├── readability.py / reliability.py / searchability.py / ner.py / ... │ ├── mufi.py / abbreviations.py / unicode_blocks.py / roman_numerals.py │ ├── pipeline_benchmark.py / pipeline_comparison.py / pipeline_spec_loader.py │ └── narrative/ moteur narratif (arbiter, renderer, registry, │ 18 détecteurs en 6 familles : ranking, pareto, │ stratum, quality, history, ensemble) │ ├── engines/ Cercle 2 — adapters OCR (5) │ ├── base.py BaseOCREngine (hérite de BaseModule) │ ├── tesseract.py / pero_ocr.py │ ├── mistral_ocr.py / google_vision.py / azure_doc_intel.py │ ├── llm/ Cercle 2 — adapters LLM (4) │ ├── base.py / mistral_adapter.py / openai_adapter.py │ └── anthropic_adapter.py / ollama_adapter.py │ ├── pipelines/ Cercle 2 — pipelines OCR+LLM intégrés │ ├── base.py (OCRLLMPipeline) / over_normalization.py │ ├── modules/ Cercle 2 — modules BaseModule officiels │ └── alto_text_to_mono_region.py │ ├── extras/ Cercle 3 — plugins / extensions │ └── importers/ IIIF, Gallica, HTR-United, HuggingFace, eScriptorium │ ├── report/ Cercle 3 — rendu HTML │ ├── generator.py / colors.py / diff_utils.py │ ├── views/ 5 vues thématiques │ ├── templates/ / i18n/ / glossary/ / vendor/ │ └── *_render.py ~22 renderers (calibration, NER, Pareto, etc.) │ ├── cli/ Cercle 3 — Click (7 fichiers) ├── web/ Cercle 3 — FastAPI (app.py, jobs.py) ├── prompts/ 8 fichiers .txt FR+EN ├── data/ Tables indicatives (pricing.yaml) └── fixtures.py Corpus de test fictifs ``` --- ## État des tests et bugs historiques **État actuel (Sprint 16)** : `pytest tests/` → **1072 passed, 2 skipped, 0 failed**. Les deux tests skip sont volontaires (dépendance scipy optionnelle). ### Bugs documentés antérieurement — tous résolus | Bug | Statut | Sprint de résolution | |-----|--------|---------------------| | Pipeline OCR+LLM sortie vide (`tesseract → ministral-3b-latest`) | ✅ Résolu | Sprint 15 — adapter Mistral logue `finish_reason`, `completion_tokens`, normalise les ContentChunk | | CI `python-multipart` manquant | ✅ Résolu | `pyproject.toml` expose `python-multipart>=0.0.9` dans les extras `dev` ET `web`; `ci.yml:71` installe `.[dev,web]` | | Tests fixtures post-Sprint 10 (counts moteurs, flag `is_pipeline`) | ✅ Résolu | Fixtures mises à jour | | Test Windows SQLite `test_history_empty_db` | ✅ Résolu | `try/except OSError` + `gc.collect()` avant `unlink` | | Test HuggingFace `test_search_language_filter` | ✅ Résolu | Assertion corrigée | En cas de régression sur un de ces bugs, chercher les fichiers de test correspondants (`test_sprint15_llm_pipeline_bugs.py`, `test_sprint8_escriptorium_gallica.py`, `test_sprint6_web_interface.py`) avant de ré-ouvrir une enquête. --- ## Règles importantes — ne pas toucher - **Ne jamais retirer `python-multipart` des dépendances** : FastAPI vérifie sa présence à l'import du module (décoration `@app.post` avec `UploadFile`), pas à l'exécution. Ça casse tous les tests web au setup. - **Ne jamais mettre `except Exception: pass`** : remplacer par `logger.warning("[module] fonctionnalité dégradée : %s", e)`. - **Toujours utiliser `logger.warning` avec message explicite** quand une fonctionnalité optionnelle échoue (confusion, taxonomy, structure, image_quality, etc.). - **Avant tout push, lancer `make lint`** (ou `ruff check picarones/ tests/`). La config est centralisée dans `pyproject.toml` sous `[tool.ruff]`, donc CI, Makefile et invocation directe produisent le même résultat. Le job `lint` du CI est bloquant — un F401 (import inutilisé) ou un E741 (variable ambiguë) fait échouer la PR, par design. - **Les profils de normalisation** sont dans `picarones/measurements/normalization.py` — l'endpoint `/api/normalization/profiles` doit les lire dynamiquement depuis ce fichier, pas depuis une liste statique. --- ## Variables d'environnement ```bash # Clés API LLM (configurées dans HuggingFace Space Settings → Variables and secrets) MISTRAL_API_KEY=... OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... # OCR cloud (optionnel) GOOGLE_APPLICATION_CREDENTIALS=/path/to/creds.json AZURE_DOC_INTEL_ENDPOINT=https://... AZURE_DOC_INTEL_KEY=... ``` --- ## Pipelines OCR+LLM — modes | Mode | Description | |------|-------------| | **zero_shot** | Le LLM reçoit l'image directement et transcrit sans OCR préalable (VLM) | | **post_correction_texte** | OCR → texte brut → LLM corrige le texte (modèles texte seul) | | **post_correction_image_texte** | OCR → LLM reçoit image + texte brut pour correction (VLM) | `ministral-3b-latest` = modèle texte pur → utiliser mode `post_correction_texte` uniquement. --- ## CI/CD - **CI GitHub Actions** : `.github/workflows/ci.yml` — Python 3.11/3.12, Linux/macOS/Windows - **Sync HuggingFace** : `.github/workflows/sync_to_huggingface.yml` — push auto sur main (nécessite secret `HF_TOKEN` dans GitHub Settings → Secrets → Actions) - **HuggingFace Space** : Docker sur port 7860 --- ## Sprints réalisés | Sprint | Contenu | |--------|---------| | 1 | Structure Python, Tesseract, Pero OCR, CER/WER, CLI | | 2 | Rapport HTML v1 (Chart.js, diff coloré, galerie) | | 3 | Pipelines OCR+LLM (3 modes), GPT-4o/Claude/Mistral/Ollama, prompts versionnés | | 4 | Adaptateurs API OCR (Mistral OCR, Google Vision, Azure), import IIIF, CER diplomatique | | 5 | Métriques avancées (unicode, ligatures, structure, qualité image, taxonomie 9 classes) | | 6 | Interface web FastAPI, HTR-United/HuggingFace, bilingue FR/EN, upload ZIP | | 7 | Rapport HTML v2 (Wilcoxon, bootstrap, clustering, score difficulté, URL stateful, CSV) | | 8 | eScriptorium, Gallica API, suivi longitudinal SQLite, analyse robustesse | | 9 | Documentation, packaging, Docker, CI/CD GitHub Actions, PyInstaller, version 1.0.0-Beta | | 10 | Distribution erreurs par ligne (Gini, percentiles), détection hallucinations VLM | | 11 | Internationalisation FR/EN, profils normalisation anglais (early_modern, medieval, secretary_hand) | | 12 | Upload ZIP depuis navigateur, filtrage fichiers macOS `._*`, profils exclusion caractères, sélecteur modèles dynamique | | 13 | Nettoyage pyproject.toml, exceptions silencieuses → warnings, parallélisation runner (ThreadPool/ProcessPool), timeout par doc, résultats partiels NDJSON, validation statistique Wilcoxon | | 14 | Filtrage robuste des moteurs, validation corpus | | 15 | Correction du bug pipeline OCR+LLM sortie vide (normalisation ContentChunk Mistral, logs finish_reason/tokens) | | 16 | **Sprint 1 du plan rapport** : câblage de `line_metrics` et `hallucination` dans le runner et l'agrégation `EngineReport`, fondations du moteur narratif (`core/narrative/` avec modèle `Fact` et registre de détecteurs), correctifs qualité (deprecation Pillow `getdata` → `tobytes`, deux `except Exception: pass` remplacés par warnings explicites) | | 17 | **Sprint 2 du plan rapport** : refactor de `generator.py` (3690 → 617 lignes) via Jinja2. Le monolithe `_HTML_TEMPLATE` est découpé en 10 fichiers externes dans `picarones/report/templates/` (base + 5 vues + header/footer + CSS + JS). L'i18n `i18n.py` (dict Python 101 clés) migré vers `picarones/report/i18n/{fr,en}.json` chargés à l'import. Ajout de 16 tests de non-régression (structure, déterminisme, i18n, garde-fous contre balises dupliquées). | | 18 | **Sprint 3 du plan rapport** : test de Friedman multi-moteurs + post-hoc Nemenyi + Critical Difference Diagram (Demšar 2006). Nouveau module `picarones/measurements/statistics/` (sous-package depuis le sprint « découpage de statistics.py », anciennement `statistics.py`) : `friedman_test`, `nemenyi_posthoc`, `build_critical_difference_svg` avec table Nemenyi (k=2 à 50, α=0,05 et 0,01), fallback pur Python (Wilson-Hilferty pour chi²), support scipy optionnel (extra `stats`). Partial `_critical_difference.html` inséré en tête du rapport, SVG rendu server-side (pas de JS), i18n FR/EN pour les aides. Détecteur narratif `detect_statistical_tie` activé (lit `nemenyi.tied_groups`). 41 tests ajoutés (cas canoniques, dégénérés, SVG, intégration rapport). | | 19 | **Sprint 4 du plan rapport** : moteur narratif complet + synthèse factuelle en tête. 9 détecteurs implémentés (global_leader_cer, significant_gap, stratum_winner/collapse, error_profile_outlier, llm_hallucination_flag, robustness_fragile, speed_winner, confidence_warning). Arbitre (`arbiter.py`) avec tri par importance, non-redondance, suppression des contradictions Wilcoxon/Nemenyi. Renderer (`renderer.py`) lit templates YAML `core/narrative/templates/{fr,en}.yaml` (10 templates par langue) et rend par `str.format_map` déterministe. Nouveau partial `_narrative_summary.html` placé en tête du rapport (entre header et CDD). Garde-fou anti-hallucination testé : chaque nombre rendu est traçable au payload du Fact associé. 32 tests (détecteurs unitaires, arbitre, renderer, E2E, traçabilité, intégration HTML). `pareto_alternative` et `cost_outlier` restent stubs pour Sprint 5. | | 20 | **Sprint 5 du plan rapport** : modélisation coût + vue Pareto. Nouveau module `picarones/measurements/pricing.py` (`EngineCost`, `estimate_cost`, `build_costs_for_benchmark`) lit la table indicative `picarones/data/pricing.yaml` (OCR locaux + APIs cloud + LLM). Nouvel algo `compute_pareto_front` dans `picarones/measurements/statistics/pareto.py`, multi-objectifs (min/max), N dimensions. Vue Chart.js dans `view_analyses.html` avec front Pareto en surbrillance et 3 toggles d'axe : coût € / vitesse / carbone (dernier étiqueté ⚗ expérimental). Détecteurs `pareto_alternative` et `cost_outlier` activés. Templates FR/EN ajoutés. Bloc "hypothèses détaillées" replié sous le graphique avec liens vers les sources de prix. 28 tests (pricing local vs cloud, override taux horaire, pareto canonique/dégénéré/3D, détecteurs, intégration HTML). | | 21 | **Sprint 6 du plan rapport** : glossaire contextuel + panneau « Mode avancé ». Nouveau module `picarones/report/glossary/` avec loader YAML et 25 entrées bilingues (CER et variantes, WER/MER/WIL, ligatures, diacritiques, taxonomie, Gini, hallucinations, bootstrap, Wilcoxon, Friedman, Nemenyi, CDD, Pareto, difficulté, normalisation, structure, qualité image) — chaque entrée porte `definition`, `measures`, `usage`, `limits`, `reference`. Dans le rapport, un petit `?` apparaît à côté de chaque en-tête de colonne pertinente ; un clic ouvre un panneau latéral avec l'entrée complète. Bouton « ⚙ Avancé » dans la nav ouvre un second panneau latéral avec : choix de colonnes visibles, filtres par strate (script_type), et vue opt-in « score composite personnel » — tous les curseurs à 0 par défaut, formule affichée en permanence, warning explicite « il n'existe pas de pondération universellement valide ». État persisté en URL (`?hidden=…&strata_off=…&w=…`). 19 nouvelles clés i18n (`glossary_*`, `customize_*`). 21 tests (loader, complétude FR/EN, structure des entrées, pas de HTML injecté, intégration rapport, garde-fou anti-prescription). | | 22 | **Sprint 7 du plan rapport (clôture phase 0)** : études de cas, documentation utilisateur, documentation développeur. Création de `docs/case-studies/` avec 2 cas d'école explicitement étiquetés (registres paroissiaux XVIIᵉ-XVIIIᵉ pour archivistes ; édition critique d'un manuscrit médiéval pour philologues). Encart sous la synthèse pointant vers le dossier. Documentation utilisateur `docs/user/reading-a-report.md` (anatomie du rapport, ordre de lecture suggéré, panneau avancé). Trois guides développeur (`docs/developer/index.md`, `narrative-engine.md`, `extending-glossary.md`, `extending-i18n.md`) couvrant l'extension de chaque sous-système. Tests E2E sur petits/grands corpus + locale EN, garde-fou « pas de fausses études prétendant être réelles » (chaque .md case-study doit contenir « Cas d'école »). 18 tests Sprint 22. | | 23-31 | Sprints intermédiaires : anti-hallucination, sécurité institutionnelle, refactor frontend Jinja2, persistance SQLite des jobs, snapshots reproductibilité, save/load config + comparaison de runs, registre déclaratif des détecteurs, polish/a11y/DX, couverture des modules sous-testés. Voir `CHANGELOG.md` [1.1.x] pour le détail. | | 32 | **Sprint 1 du plan d'évolution 2026 — Phase 0.1 : GT multi-niveaux**. Refonte de `picarones/core/corpus.py` pour porter une vérité terrain à plusieurs niveaux (`GTLevel.{TEXT,ALTO,PAGE,ENTITIES,READING_ORDER}`), payloads typés (`TextGT`, `AltoGT`, `PageGT`, `EntitiesGT`, `ReadingOrderGT`) avec `source_path` traçable. Le champ `Document.ground_truth: str` reste la source de vérité historique et est synchronisé automatiquement avec `Document.ground_truths[GTLevel.TEXT]` — rétrocompatibilité stricte (1478 tests existants passent sans modification). Le loader détecte automatiquement `.gt.alto.xml`, `.gt.page.xml`, `.gt.entities.json`, `.gt.reading_order.json` à côté de l'image. `Corpus.gt_level_coverage()` et `Corpus.available_gt_levels` exposent la couverture. Erreurs de parse dégradées en `logger.warning` (jamais `except: pass`). +17 tests dans `test_sprint32_multi_level_gt.py`. **Verrou levé** : ce sprint débloque l'évaluation des modules qui produisent ou consomment ALTO/PAGE/entités (axe B du plan, à venir Sprint 35+) et plusieurs métriques de l'axe A (Layout F1, reading order F1, NER). | | 33 | **Sprint 2 du plan d'évolution 2026 — Phase 0.2 : interface module générique**. Nouveau module `picarones/core/modules.py` avec l'enum `ArtifactType` (IMAGE, TEXT, ALTO, PAGE, ENTITIES, READING_ORDER) et la classe abstraite `BaseModule` qui déclare `input_types`/`output_types`, `execution_mode` (`"io"`/`"cpu"`), une méthode `process(dict[ArtifactType, Any]) → dict[ArtifactType, Any]`, et des helpers `validate_inputs`/`validate_outputs`. `BaseOCREngine` (`picarones/engines/base.py`) hérite désormais de `BaseModule` avec `input_types=(IMAGE,)` et `output_types=(TEXT,)` ; sa nouvelle méthode `process` wrappe l'API historique `run()`. Aucun adaptateur OCR existant n'est touché — `test_engines.py` passe à 20/20 sans modification. +23 tests dans `test_sprint33_module_interface.py` (contrat, validation, MockModule TEXT→ALTO démonstratif comme demandé par le plan, délégation `BaseOCREngine.process → run`, cohérence ArtifactType/GTLevel). **Verrou levé** : un même runner peut maintenant exécuter un OCR (image→texte), un mappeur VLM→ALTO, un rewriter ALTO→ALTO, un module NER (texte→entités), etc. — fondation directe pour l'axe B du plan. | | 34 | **Sprint 3 du plan d'évolution 2026 — Phase 0.3 : registre typé de métriques (clôture Phase 0)**. Nouveaux modules `picarones/core/metric_registry.py` (`MetricSpec`, `@register_metric`, `select_metrics`, `compute_at_junction`) et `picarones/measurements/builtin_metrics.py` qui enregistre `cer`, `wer`, `mer`, `wil` sur `(TEXT, TEXT)` plus un stub `text_preservation_after_reconstruction` sur `(TEXT, ALTO)` comme preuve de concept de jonction hétérogène. **Approche strictement additive** : ni `metrics.py` ni `compute_metrics` ne sont modifiés, le rapport HTML reste identique octet par octet. La sélection par signature de types est exacte (pas de coercion). +21 tests dans `test_sprint34_metric_registry.py`, dont une parité numérique CER/WER/MER/WIL avec `compute_metrics` legacy à 1e-9 près sur 4 paires de textes. **Verrou levé** : le runner d'une pipeline composée peut maintenant calculer automatiquement la métrique adéquate à chaque jonction de son DAG selon les types d'artefacts produits/attendus — fondation directe pour la métrique d'absorption d'erreur (acte B.3) et toutes les métriques structurelles à venir (Layout F1, reading order F1, NER). | | 35 | **Sprint 4 du plan d'évolution 2026 — Étape 2 / axe A : métriques inter-moteurs (couche de calcul)**. Nouveau module `picarones/measurements/inter_engine.py` qui répond à deux questions distinctes mais liées : *(a) à quel point les moteurs font-ils des erreurs de natures différentes ?* via `kl_divergence`, `jensen_shannon_divergence` (symétrique, bornée `[0, 1]`), et `taxonomy_divergence_matrix` qui construit la matrice triangulaire inter-moteurs ; *(b) quel CER serait atteignable si on combinait les moteurs ?* via `oracle_token_recall` (proxy bag-of-words, borne supérieure du recall atteignable), `complementarity_gap` (oracle vs meilleur moteur seul, gap absolu/relatif), et `pairwise_disagreement_rate`. Fonctions pures, sans I/O ni intégration runner — la couche de calcul est livrée indépendamment, le câblage narratif (`ENSEMBLE_OPPORTUNITY`) et HTML (matrice de divergence, badge oracle) suit au Sprint 36. +27 tests couvrant les invariants mathématiques (KL ≥ 0, KL(p,p) = 0, JS symétrique et bornée, oracle ≥ best_single, multiplicité respectée), les cas concrets (deux moteurs spécialisés sortent comme candidats ensemble, complémentarité parfaite atteint oracle = 1), et les garde-fous (référence vide, hypothèses vides, métrique inconnue). | | 97 | **Sprint 66 du plan d'évolution 2026 — B.6 : politique de modules contribués (manifest + audit + vue HTML + doc)**. Avant d'ouvrir Picarones aux contributions externes (axe B), il faut un cadre de qualité : *« un module qui ne passe pas l'audit n'est pas exécutable. »* Nouveau module `picarones/measurements/module_policy.py` : dataclass `ModuleManifest` avec 5 champs obligatoires (name, version, author, license, description) + input/output_types non vides + optionnels (citation BibTeX/DOI, homepage, picarones_min_version, extra) ; pas de validation SPDX (l'outil documente, ne juge pas) ; `validate_manifest` → liste d'AuditCheck ; dataclasses `AuditCheck(name, passed, detail)` + `AuditResult(module_name, passed, checks)` avec n_passed/n_failed properties ; `audit_module(class_or_instance, manifest)` ajoute 4 checks au manifest (héritage BaseModule Sprint 33, I/O match case-insensitive `"TEXT"` ↔ `"text"`, process callable). Module de rendu `picarones/report/module_audit_render.py` : tableau récapitulatif modules × {audit ✓/✗, version, auteur, licence, I/O, citation tronquée 120 chars, homepage tronquée 80 chars sans auto-link}. Adaptive masking, anti-injection systématique. Documentation `docs/developer/module-policy.md` (135 lignes) avec TL;DR + table des champs + contrat BaseModule + audit + **stratégie d'ouverture en 2 temps** (fermée actuelle → ouverte via plugins PyPI `picarones-module-X` avec `entry_points` quand 5–6 modules officiels stables). +12 clés i18n FR/EN (`audit_*`). +23 tests dans `test_sprint97_module_policy.py` (ModuleManifest as_dict + optionnels, validate_manifest 4 cas, audit_module 6 cas dont **case-insensitive sur les types** prouvant `"TEXT"` ↔ `ArtifactType.TEXT` équivalents, vue HTML 6 cas dont badge ✓/✗ + anti-injection sur name/homepage/citation + EN, présence doc + listing champs obligatoires, complétude i18n 12 clés). **Verrou levé** : la phase fermée a son cadre formel ; la phase ouverte peut être déclenchée sans refactor d'interface — tout module externe devra fournir un manifest valide et passer l'audit. | | 96 | **Sprint 65 du plan d'évolution 2026 — B.5 : comparaison incrémentale (couche calcul + vue HTML ANOVA-like)**. Avec 5 OCR × 3 reconstructeurs × 4 post-correcteurs × 3 mappeurs = 180 pipelines à comparer, le rapport noie l'information. Il faut un mécanisme de comparaison contrôlée type design d'expérience. Nouveau module `picarones/measurements/incremental_comparison.py` : dataclass immuable `PipelineRun(name, slots, score)` décrivant un run avec sa signature de modules ; `compare_isolated_effect(runs, varying_slot, higher_is_better=False)` mesure l'effet isolé d'un slot en fixant tous les autres, retourne pour chaque valeur du slot variant `{n_observations, mean, stdev, min, max, mean_rank}` + `best_value`/`worst_value`. Ex aequo → rangs moyens partagés. Garde-fous : `None` si <2 runs ou slot inconnu, schémas incompatibles ignorés. Accepte PipelineRun ou dicts. Module de rendu `picarones/report/incremental_comparison_render.py` : tableau ANOVA-like trié par rang moyen ascendant, mean coloré gradient vert (meilleur) → rouge (pire), best ★ vert, worst ▼ rouge. Adaptive masking. Anti-injection sur valeur du slot ET sur nom de slot variant. Pas de tests statistiques recalculés (Friedman/Nemenyi existent déjà Sprint 18). +9 clés i18n FR/EN (`incr_*`). +20 tests dans `test_sprint96_incremental_comparison.py` (4×2 → gpt rang 1.0, higher_is_better, lt 2, slot inconnu, schémas incompatibles, dicts, ex aequo, **cas réaliste 5 OCR × 2 LLM**, vue HTML tri + marqueurs + anti-injection + EN, complétude i18n 9 clés). **Verrou levé** : un bench d'axe B avec dizaines de pipelines voit immédiatement *« en variant le LLM, gpt-4o domine sur 100 % des configurations (rang moyen 1.0) »*. | | 95 | **Sprint 64 du plan d'évolution 2026 — B.4 : visualisation DAG d'un pipeline composé (rendu SVG server-side)**. Outil d'**inspection**, pas de construction — le YAML reste source de vérité. Nouveau module `picarones/report/pipeline_dag_render.py` : `build_pipeline_dag_html(nodes, labels, edges=None, thresholds=(0.05, 0.15), higher_is_better=False)` rend un graphe orienté gauche → droite en SVG natif (pas de bibliothèque, pas de JS). Nœuds = rectangles avec nom + input/output types. Arêtes = flèches colorées vert/orange/rouge selon la valeur de métrique à la jonction, avec étiquette `type + métrique : valeur` (formatée %). Légende intégrée. Mode `higher_is_better=True` inverse la sémantique pour F1/recall. Adaptive : `""` si moins d'un nœud. Auto-déduction d'arêtes séquentielles si non fournies. Anti-injection systématique sur 4 vecteurs (nom nœud, artifact_type, metric_name, input/output_types). Pas de drag-and-drop, pas de drill-down par document — le visuel sert à inspecter et déboguer, pas à construire. Le drill-down reste dans `error_absorption` (Sprint 94). +6 clés i18n FR/EN (`dag_*`). +18 tests dans `test_sprint95_pipeline_dag.py` (vide, single node, 2 nœuds 1 arête, chaîne 3 nœuds, auto-edges, 3 couleurs sur seuil, higher_is_better, ghost node skipped, valeur absente, anti-injection 4 vecteurs, rendu EN, complétude i18n 6 clés). **Verrou levé** : un benchmark d'axe B voit immédiatement à quelle jonction la qualité décroche, sans parcourir un tableau de métriques. | | 94 | **Sprint 63 du plan d'évolution 2026 — B.3 : métrique d'absorption d'erreur (couche calcul + vue HTML)**. Quand un module post-correction LLM aplatit les différences entre OCR amont, ce n'est pas qu'il « améliore » tous les moteurs — c'est qu'il introduit ses propres biais qui dominent ceux de l'OCR. À chaque jonction, deux flux séparés : taux de correction (parmi les erreurs avant, combien corrigées) et taux d'introduction (parmi les erreurs après, combien nouvelles). Nouveau module `picarones/measurements/error_absorption.py` : `compute_error_absorption(reference, before, after, case_sensitive=False)` alignement multi-set token-level sur whitespace, retourne `{n_gt_tokens, n_errors_before, n_errors_after, n_corrected, n_introduced, n_kept_wrong, correction_rate (None si 0 err avant), introduction_rate (None si 0 err après), net_improvement, corrected_tokens, introduced_tokens (casse GT)}`. None si GT vide. `aggregate_error_absorption(per_doc, sample_tokens=50)` somme corpus-wide + recalcul micro + cap échantillon. Généralisation du score sur-normalisation (A.I.7) à toute jonction OCR→LLM/OCR→reconstructor/VLM→ALTO_mapper. Pas de classification d'erreur (volume, pas qualité — taxonomy reste dans Sprint 5). Module de rendu `picarones/report/error_absorption_render.py` : tableau résumé jonctions × {erreurs avant, après, corrigées coloré vert, introduites coloré rouge, % corrigées (rouge → vert), % introduites (vert → rouge), amélioration nette colorée selon signe + magnitude, échantillon tokens introduits cap}. Adaptive masking. Module pur — l'utilisateur compose les `junctions` depuis `PipelineBenchmarkResult` (Sprint 64). Visualisation Sankey reportée. +11 clés i18n FR/EN (`absorption_*`). +20 tests dans `test_sprint94_error_absorption.py` (identité, perfect correction, pure introduction, **cas réaliste mix maistre Pierre du Bois → maître Pierre du Bois** corrige+introduit en parallèle, GT vide → None, case-insensitive + opt-in, multiplicité, agrégation micro-rate + skip None + cap, vue HTML 4 cas dont anti-injection junction_name + échantillon introduits + FR + EN, complétude i18n 11 clés). **Verrou levé** : un bench de pipeline composée distingue désormais un module qui *corrige* d'un module qui *absorbe* — *« le LLM corrige 65 % des erreurs OCR mais introduit 12 % de nouvelles erreurs (modernisations maistre/nostre) »*. Sans cette métrique, on confondait correction et écrasement. | | 93 | **Sprint 62 du plan d'évolution 2026 — A.II.7 : métriques d'image prédictives (couche calcul + vue HTML)**. `image_quality.py` (Sprint 5) mesurait des features indépendamment ; ce module les combine en deux indicateurs corpus-level. Nouveau module `picarones/measurements/image_predictive.py` : `compute_paleographic_complexity(quality, weights)` retourne score ∈ [0,1] + components + weights_used (combinaison pondérée éditoriale 0.30 noise / 0.30 blur / 0.20 low_contrast / 0.20 rotation, bornes forcées) ; `compute_corpus_homogeneity(image_qualities)` retourne score ∈ [0,1] (moyenne des écart-types normalisés sur 4 features) + n_docs + per_feature, 0 = uniforme (moyenne globale fiable), 1 = très hétérogène ; `aggregate_corpus_predictive` synthétise complexité (mean/median/min/max/stdev) + homogeneity. Pas de prédiction CER absolue (philosophie banc d'essai exclut un modèle entraîné par moteur). Module de rendu `picarones/report/image_predictive_render.py` : 2 blocs — tableau résumé complexité (mean coloré gradient vert → rouge, médiane, min, max, stdev, docs) + tableau homogénéité (score coloré + détail par feature mean/stdev/contribution normalisée). Adaptive masking. Module pur — l'utilisateur compose. +20 clés i18n FR/EN (`imgpred_*`). +21 tests dans `test_sprint93_image_predictive.py` (cas trivial → ≈0, cas extrême → ≈1, bornes [0,1], poids custom, défauts somment à 1, garde-fous None ; corpus uniforme → 0, hétérogène > 0.5, lt 2 → None ; cas réaliste BnF mix trivial/difficile ; vue HTML 4 cas dont anti-injection FR + EN ; complétude i18n 19 clés). **Verrou levé** : un benchmark BnF voit désormais *« corpus-wide complexity 0,42 (modérée), homogeneity 0,18 (uniforme — moyenne fiable) »* dans la vue Analyses — explique une partie du CER observé sans prédiction prescriptive. | | 92 | **Sprint 61 du plan d'évolution 2026 — A.II.9 : métriques longitudinales (régression linéaire + change-point + détecteur narratif + vue HTML)**. L'historique SQLite (Sprint 8) collectait sans qu'aucune métrique n'en sorte. Complémentaire à A.I.3 qui dit *« écart anormal sur ce corpus »* sans caractériser la dynamique. Nouveau module `picarones/measurements/longitudinal.py` : `compute_linear_trend` régression OLS pure Python sans scipy retourne `LinearTrend(slope, intercept, r_squared, n_runs)` ; `detect_change_point(series, min_segment_size=3)` balayage exhaustif (Pettitt simplifié) retourne `ChangePointResult(index, timestamp, mean_before, mean_after, delta, n_before, n_after)` ; `compute_engine_longitudinal` combine les deux avec garde-fou `min_runs_for_trend=3` et seuil `change_point_threshold=0.01` (1 pt CER) ; `compute_corpus_longitudinal` agrège tous les moteurs. Nouveau `FactType.REGRESSION_IN_HISTORY` (priority 170, MEDIUM par défaut, HIGH si `|absolute_delta| ≥ 0.05`) + détecteur lit `benchmark_data["longitudinal_trends"]`, déclenche si pente > +1 pt CER/an **ou** change-point delta > 1 pt CER, payload trace `pattern in {"trend", "change_point", "trend_and_change_point"}`. Templates FR/EN sans chiffres en dur. Ajout aux paires complémentaires : `(GLOBAL_LEADER_CER, REGRESSION_IN_HISTORY)` et `(ENGINE_OFF_BASELINE, REGRESSION_IN_HISTORY)`. Module de rendu `picarones/report/longitudinal_render.py` : tableau moteur × {n_runs, premier CER, dernier CER, Δ cumulé coloré (vert→orange→rouge sur ±5 pts ; bleu si amélioration), pente annualisée, R², point de rupture avec timestamp + delta}. Tri par Δ décroissant. Adaptive masking. +10 clés i18n FR/EN (`longitudinal_*`). +28 tests (régression OLS, change-point, intégration entries + filtre corpus + min_runs + threshold, multi-moteurs, détecteur 6 cas, **traçabilité anti-hallucination FR + EN** sur sentences de `build_synthesis`, vue HTML 4 cas dont anti-injection, complétude i18n 10 clés). **Verrou levé** : un benchmark voit désormais *« sur les 8 runs historiques pour tess, le CER moyen est passé de 4 % à 7 % (variation cumulée 3 points) »* — permet de relier une régression à un changement de pipeline. | | 91 | **Sprint 60 du plan d'évolution 2026 — A.II.6 : métriques économiques (throughput effectif + coût marginal par erreur évitée, couche calcul + vue HTML throughput)**. Le throughput brut ment quand un moteur est rapide mais imprécis : la correction humaine *post hoc* absorbe le gain. Discrimine fortement entre cloud rapide à 30 % de timeouts et local lent à 100 % de fiabilité. Nouveau module `picarones/measurements/throughput.py` : `compute_effective_throughput(n_pages, duration_seconds, n_errors, time_per_error_seconds=5.0)` retourne `{n_pages, duration_seconds, n_errors, time_per_error_seconds, correction_time_seconds, total_seconds, pages_per_hour_raw, pages_per_hour_effective, drag_ratio}`. Constante HTR-United (5 s/erreur) surchargeable. Garde-fous : `None` si `n_pages = 0` ou `total_seconds = 0`, `ValueError` sur valeurs négatives. `aggregate_effective_throughput(per_engine)` agrège par moteur. Nouveau module `picarones/measurements/marginal_cost.py` : `compute_marginal_cost(cost_a, errors_a, cost_b, errors_b)` retourne `{cost_per_avoided_error, n_errors_avoided, cost_delta, dominated}` ou `None` si `errors_b ≥ errors_a`. `dominated=True` quand B moins cher ET plus précis. `compute_marginal_cost_matrix(per_engine)` retourne paires ordonnées (A → B) triées par coût marginal croissant. Nouveau module `picarones/report/throughput_render.py` : `build_throughput_html(aggregated, labels)` produit tableau résumé moteur × {pages/h brut, pages/h **utilisable** (gradient rouge → vert sur le max observé), % drag (gradient vert → rouge), pages, erreurs}, tri par pages/h utilisable décroissant. Adaptive : `""` si pas de données. Module pur — l'utilisateur compose la liste `per_engine`. Vue HTML coût marginal couplée à la vue Pareto reportée à un sprint ultérieur. +9 clés i18n FR/EN (`throughput_*`). +27 tests dans `test_sprint91_throughput.py` (formule effective avec/sans erreurs, custom time_per_error, garde-fous, drag_ratio élevé, agrégation 3 cas, marginal cost 5 cas dont dominé/non comparable, matrice tri ascendant + lt 2 + données invalides, **cas réaliste BnF** Tesseract local 600 p/h brut → 423 p/h effectif vs GPT-4o cloud 1800 p/h brut → 300 p/h effectif, vue HTML 4 cas dont anti-injection + tri descendant, complétude i18n 9 clés). **Verrou levé** : un archiviste BnF qui pondère un budget contre une exigence de délai voit immédiatement *« Tesseract local 423 p/h utilisable, GPT-4o cloud 300 p/h utilisable malgré son apparente vitesse de 1800 p/h brut »* — la décision business s'aligne sur la réalité opérationnelle. | | 90 | **Sprint 59 du plan d'évolution 2026 — A.II.4 finition : détecteur narratif `engine_unstable` + vue HTML stabilité multi-runs**. Le module `picarones/measurements/reliability.py` (Sprint 83) livrait la couche de calcul ; aucun détecteur ni vue ne consommaient les données. Critique pour les moteurs LLM/VLM dont la non-déterministie sape la reproductibilité scientifique. Nouveau `FactType.ENGINE_UNSTABLE` (priority 160, importance HIGH) + détecteur `detect_engine_unstable` qui lit `benchmark_data["multirun_stability"]` (liste enrichie d'`engine_name` + sortie de `compute_multirun_stability`). Garde-fous : `n_runs ≥ 2`, déclenche si `cer_cv > 0.10` **ou** `identical_run_rate < 0.50`. Templates FR/EN sans chiffres en dur. Ajout du couple `(GLOBAL_LEADER_CER, ENGINE_UNSTABLE)` à `_COMPLEMENTARY_PAIRS` de l'arbitre — un moteur peut être leader **et** instable, et c'est précisément l'information critique à remonter ensemble. Nouveau module `picarones/report/multirun_stability_render.py` : `build_multirun_stability_html(stability, labels)` rend un tableau moteur × {n_runs, CER moyen ± σ, CV (gradient vert→orange→rouge sur 0–25 %), % runs identiques, sorties distinctes}. Adaptive : `""` si liste vide ou tous `cer_cv` None. Note d'intégration : la vue est un module pur (l'utilisateur exécute lui-même les N runs ; option runner `--repeats N` reportée à un sprint dédié). +8 clés i18n FR/EN (`stability_*`). +18 tests dans `test_sprint90_engine_unstable.py` (FactType + arbiter, détecteur 6 cas, **traçabilité anti-hallucination FR + EN** sur les sentences de `build_synthesis`, vue HTML 4 cas dont anti-injection, complétude i18n 8 clés). **Verrou levé** : un papier scientifique qui rapporte un CER LLM voit désormais *« sur 4 runs successifs, gpt-4o produit des sorties variables (CV 24,3 %) — interpréter avec prudence »* dans la synthèse + le tableau de stabilité dans la vue. | | 89 | **Sprint 58 du plan d'évolution 2026 — A.II.8b : score de spécialisation inter-moteurs (couche calcul + vue HTML)**. La matrice de divergence taxonomique (Sprint 35) répondait à *« à quel point ces moteurs se trompent-ils différemment ? »* ; ce sprint transforme cette information en un score lisible et un **top-N des paires les plus spécialisées**, qui répond directement à la question *« quels moteurs sont des candidats pour un voting ensemble ? »*. Le module **ne recommande pas** d'ensemble — observation factuelle, le chercheur arbitre. Nouveau module `picarones/measurements/specialization.py` : `compute_specialization_score(taxonomy_a, taxonomy_b)` retourne un score normalisé ∈ [0, 1] (délégué à `inter_engine.jensen_shannon_divergence` Sprint 35, pas de double calcul) ; `classify_specialization(score)` classe en `similar` (< 0,10) / `distinct` (0,10–0,30) / `highly_specialized` (≥ 0,30) — seuils éditoriaux pas verdict, surchargeables ; `compute_specialization_matrix(taxonomies)` retourne matrice symétrique avec `max_pair` ; `top_specialized_pairs(matrix, n=5, min_score=0)` retourne paires triées par score décroissant + catégorie. Nouveau module `picarones/report/specialization_render.py` : `build_specialization_html` rend tableau Moteur A × Moteur B × Score (gradient blanc → bleu profond) × Lecture (libellé i18n). Adaptive : `""` si < 2 moteurs avec taxonomie. Anti-injection. Câblage générator : lit `aggregated_taxonomy` exposés sur les moteurs (Sprint 5/runner historique), construit map `{engine: counts}`. Insertion `view_analyses.html` derrière la lisibilité. +9 clés i18n FR/EN (`specialization_*`). +24 tests dans `test_sprint89_specialization.py` (score symétrique + identité 0 + disjoint 1 + bornes [0,1], classify 5 cas dont custom thresholds, matrice diagonale 0 + symétrique + max_pair correctement identifié, top_pairs tri/n/min_score/None, rendu adaptive + anti-injection + FR/EN, complétude i18n 9 clés). **Verrou levé** : un benchmark BnF avec ≥ 2 moteurs voit immédiatement *« tess et pero ont une spécialisation forte (0,489) — ils font des erreurs de natures différentes »* — observation factuelle. | | 88 | **Sprint 57 du plan d'évolution 2026 — A.I.8 vue HTML : déficit projeté de robustesse (clôture A.I.8 bout-en-bout)**. Le module `picarones/measurements/robustness_projection.py` (Sprint 81) calculait la projection des courbes de dégradation synthétique sur les caractéristiques d'image réelles ; ce sprint livre la **vue HTML**. La robustesse étant un workflow CLI séparé (`picarones robustness`) et non intégré au benchmark principal, ce sprint livre un **module de rendu pur** que l'utilisateur compose lui-même (`analyze_robustness` → `project_robustness_on_corpus` → `aggregate_projection_per_engine` → `build_robustness_projection_html`). Nouveau module `picarones/report/robustness_projection_render.py` : **deux tableaux** — (1) **Résumé par moteur** (déficit total avec gradient vert→orange→rouge sur ±5 pts, n types évalués, pire dégradation avec sa contribution, trié par déficit décroissant) ; (2) **Détail (moteur × dégradation)** (docs, docs avec data, déficit projeté coloré, docs au-dessus du seuil critique). Si `aggregated` non fourni, calculé automatiquement. Adaptive : `""` si projection vide. Anti-injection systématique. Note explicite que la sommation suppose l'indépendance des dégradations *« approximation utile pour le diagnostic, pas un verdict »*. +13 clés i18n FR/EN (`robproj_*`). +12 tests dans `test_sprint88_robustness_projection_html.py` (rendu vide/None, rendu complet, calcul automatique de l'agrégation, tri par déficit décroissant, formatage « pire dégradation », gestion déficit None → cellule —, anti-injection nom moteur + type dégradation, rendu FR + EN, **bout-en-bout** avec le pipeline réel `project_robustness_on_corpus` + `aggregate_projection_per_engine`, complétude i18n 13 clés). **Verrou levé** : A.I.8 livrée bout-en-bout (calcul Sprint 81 + vue HTML Sprint 88) — un benchmark BnF qui veut savoir *« mon corpus de notaires XVIIᵉ siècle est-il à risque face à mon moteur OCR ? »* obtient un tableau lisible directement intégrable dans le rapport. | | 87 | **Sprint 56 du plan d'évolution 2026 — A.II.2 (delta Flesch) câblé bout-en-bout : runner adaptive + vue HTML « Lisibilité »**. Le module `picarones/measurements/readability.py` (Sprint 52) calculait le delta Flesch *« over-normalisation par LLM »* — ce sprint le remonte automatiquement dans le rapport. Helper `picarones/measurements/readability.py` : `compute_readability_metrics(reference, hypothesis, lang)` avec **adaptive masking ≥ 5 mots GT** (Flesch instable sur très courts textes) ; `aggregate_readability_metrics` retourne `{lang, n_docs, n_docs_with_delta, delta_mean/median/min/max, n_over_normalized, n_under_normalized, over_normalized_rate}` — over-norm défini à Δ > +5 (LLM modernise un texte ancien), under-norm à Δ < -5 (dégradation OCR brutale). `DocumentResult.readability_metrics` + `EngineReport.aggregated_readability` (sérialisation conditionnelle, libérés par `compact`). Câblage runner : langue lue depuis `corpus.metadata.get("language", "fr")`, fallback fr avec warning si valeur non `fr`/`en`, paramètre `corpus_lang` propagé jusqu'aux workers IO et CPU (workers acceptent 7 ou 8 args en mode legacy pour rétrocompat). Erreur isolée par try/except + warning. Module de rendu `picarones/report/readability_render.py` : tableau résumé moteur × {Δ moyen coloré (vert au centre, orange si over-norm, bleu si under-norm), Δ médian, % over-normalisés, docs under-normalisés, docs} ; saturation à ±15 points. Insertion dans `view_analyses.html` derrière les blocs A.II.5. Anti-injection systématique. +8 clés i18n FR/EN. +20 tests dans `test_sprint87_readability_html.py` (adaptive masking GT < 5 mots, langue fr/en, hypothèse vide → flesch_delta None mais flesch_reference conservé, agrégation moyenne + over-norm rate, sérialisation `DocumentResult`/`EngineReport`, `compact`, masquage adaptatif HTML, rendu FR + EN, anti-injection, complétude i18n 8 clés). **Verrou levé** : le rapport remonte désormais *« GPT-4o : Δ moyen +11,5, 85 % des docs over-normalisés »* directement dans la vue Analyses — métrique critique pour repérer les VLM hallucinant du français moderne sur du français médiéval. Reste pour A.II.2 bout-en-bout : `reading_order_f1` et `layout_f1` (Sprints 53-54), qui requièrent un moteur produisant PAGE/ALTO et seront câblés via les pipelines composées (axe B). | | 86 | **Sprint 55 du plan d'évolution 2026 — A.II.5 : câblage runner adaptive + vues HTML (clôture A.II.5 bout-en-bout)**. Suite directe Sprints 84+85 — la couche de calcul livrait deux modules pour le mode plein-texte patrimonial, ce sprint les remonte automatiquement dans le rapport. Deux helpers `picarones/measurements/searchability.py` et `picarones/measurements/numerical_sequences.py` calculent les métriques par document avec **adaptive masking** (rien n'apparaît pour un doc sans GT exploitable) et agrègent corpus-wide en *micro*-rappel pour searchability et somme par catégorie pour les séquences numériques. `DocumentResult` gagne `searchability_metrics` + `numerical_sequence_metrics` ; `EngineReport` gagne `aggregated_searchability` + `aggregated_numerical_sequences` (sérialisation conditionnelle, libérés par `compact`). Le runner historique calcule les deux inconditionnellement (coût négligeable face à l'OCR), erreur isolée par try/except + warning explicite, rétrocompat stricte. Deux modules de rendu `picarones/report/searchability_render.py` (tableau résumé moteur × {rappel coloré rouge→jaune→vert, retrouvés/total, docs}) et `picarones/report/numerical_sequences_render.py` (tableau moteur × catégorie {year/roman/foliation/currency/regnal} avec **adaptive masking par catégorie** — une catégorie sans signal est omise pour tous les moteurs ; chaque cellule affiche le score strict en gradient + la valeur entre parenthèses + n). Insertion dans `view_analyses.html` derrière le profil philologique, `chart-card` pleine largeur conditionné. Anti-injection systématique. +15 clés i18n FR/EN (`search_*`, `numseq_*`). +25 tests dans `test_sprint86_aii5_html.py` (adaptive masking helpers, agrégation micro-rappel, somme par catégorie, sérialisation `DocumentResult`/`EngineReport`, `compact` qui efface, masquage adaptatif HTML, rendu FR + EN, anti-injection sur nom moteur, complétude i18n 15 clés). **Verrou levé** : un benchmark BnF voit désormais sur la vue Analyses *« Recherchabilité fuzzy : tess 95,2 %, pero 87,8 % »* + le tableau séquences numériques détaillé par catégorie — A.II.5 livrée bout-en-bout (calcul Sprints 84-85, runner et HTML Sprint 86). | | 85 | **Sprint 54 du plan d'évolution 2026 — A.II.5b : précision sur séquences numériques (couche de calcul + registre typé)**. Pour un économiste-historien, un éditeur de chartes ou un archiviste, la fidélité aux séquences numériques est un proxy direct de la qualité éditoriale — un OCR qui rate « 1789 » dans une charte révolutionnaire ou « f. 12v » dans une cote d'archives produit un corpus inutilisable, même avec un CER global respectable. Nouveau module `picarones/measurements/numerical_sequences.py` couvrant **5 catégories** : (1) **dates arabes** années 4 chiffres dans la plage [1000-2099], (2) **numéraux romains** délégués à `roman_numerals.detect_roman_numerals` Sprint 60, (3) **foliotation** (`f.`, `fol.`, `p.`, `pp.`, `n°`) avec suffixe `r`/`v` préservé (recto/verso = information distincte non interchangeable côté valeur), (4) **montants** Ancien Régime (`livres/l.`, `sols/s.`, `deniers/d.`) et modernes (`£`, `€`, `₣`, `écus`, `florins`, `francs`), (5) **années régnales** (`an III`, `l'an V`, `an de grâce 1450`). `compute_numerical_sequence_metrics(reference, hypothesis)` classe chaque GT en `strict_preserved` (forme exacte) / `value_preserved` (`XIV` ↔ `14` accepté ; **mais pas** `f. 12r` ↔ `f. 12v`) / `lost`. Multiplicité respectée. Retourne `{global_strict_score, global_value_score, n_total, per_category{n_total, strict, value, strict_score, value_score, lost_items}}`. `numerical_sequence_strict_score` et `numerical_sequence_value_score` enregistrés dans le registre typé Sprint 34 pour `(TEXT, TEXT)`. Limites documentées : regex conservatrices (« mil cinq cens » non détecté comme année), pas de cross-category match (`MDCLXVIII` GT et `1668` hyp sont catégorisés séparément). +27 tests dans `test_sprint85_numerical_sequences.py` couvrant détecteurs individuels, scénarios identité/perte totale/GT vide/recto-verso non interchangeables/multiplicité, **2 cas réalistes** (charte XVIIIᵉ siècle préservée vs registre paroissial où l'OCR modernise XVIII→18 mais préserve l'année 1750 et la foliation), intégration registre 4 cas. **Verrou levé** : un bench d'archive numérique peut classer ses moteurs sur la dimension *« mes dates et cotes seront-elles fiables ? »*, qui complète la **recherchabilité fuzzy** (Sprint 84) pour livrer **A.II.5 en couche de calcul intégrale**. Reste pour clôturer A.II.5 bout-en-bout : câblage runner + colonne HTML « Recherchabilité » + table HTML séquences numériques. | | 84 | **Sprint 53 du plan d'évolution 2026 — A.II.5a : recherchabilité fuzzy (couche de calcul + registre typé)**. Le CER mesure les erreurs caractère par caractère ; pour la recherche plein-texte (Elastic, Solr, full-text Gallica), la question réelle est *« combien de mots GT sont retrouvables à orthographe approchée près ? »*. Un CER de 8 % peut donner 95 % de findability si les erreurs sont sur des caractères non significatifs ; à l'inverse 4 % distribué sur tous les noms propres rend le corpus inutilisable pour l'indexation prosopographique. Nouveau module `picarones/measurements/searchability.py` : `levenshtein_distance(a, b)` DP O(|a|·|b|) mémoire O(min(|a|,|b|)) ; `compute_searchability(reference, hypothesis, max_distance=2, case_sensitive=False)` aligne par multi-set (un token hyp utilisé une seule fois, comme `rare_token_recall` Sprint 71), retourne `{n_gt_tokens, n_searchable, recall, missed_tokens, max_distance}` avec `recall=None` quand n_gt=0 (différencie GT vide de zéro match), court-circuit longueur (Levenshtein ≥ |Δlen|) et arrêt précoce sur match exact ; `searchability_recall_metric` enregistré dans le registre typé Sprint 34 pour `(TEXT, TEXT)` (convention float : 0.0 si GT vide pour cohérence runner). Défaut `max_distance=2` aligné sur Elastic `fuzziness: AUTO`. Limites documentées : tokenisation par split whitespace, Levenshtein non pondéré, pas de sémantique. +28 tests (Levenshtein 9 cas standards dont kitten classique, computation 13 cas dont identité/disjoint/GT vide/hypothèse vide/max_distance=0|2|large/casse/multiplicité/missed_tokens préserve casse GT/ValueError max_distance<0, **2 cas réalistes opposés** Charles→Charlemagne non retrouvé vs maistre→maitre retrouvé, intégration registre 4 cas dont `compute_at_junction`). **Verrou levé** : un bench BnF d'archive numérique peut désormais classer ses moteurs sur la dimension *« mes corpus seront-ils retrouvables après OCRisation ? »* — proxy direct de la valeur d'usage. | | 83 | **Sprint 52 du plan d'évolution 2026 — A.II.4 : métriques de fiabilité (couche de calcul, démarrage Étape 4 post-A.I)**. Une publication scientifique qui rapporte un CER LLM sans stabilité est méthodologiquement faible ; un benchmark qui ignore le plafond humain crée des classements faussement optimistes. Nouveau module `picarones/measurements/reliability.py` couvrant deux familles : (1) **IAA caractère** — `cohen_kappa(annotations_a, annotations_b)` retourne κ standard avec convention 1.0/0.0 documentée pour `pe=1` indéfini, garde-fous sur tailles/vide ; `krippendorff_alpha(units)` mode nominal généralisé à N annotateurs avec missing values (cellules None autorisées), formule `1 - D_o / D_e` sur paires sans remise, `None` si single label corpus-wide ou aucune unité ≥2 valides ; `_aligned_char_pairs(text_a, text_b)` aligne via `SequenceMatcher` sur opcodes `equal` et `replace` (insert/delete sans alignement bilatéral), `compute_iaa(transcription_a, transcription_b)` retourne `{n_aligned_chars, cohen_kappa, krippendorff_alpha, agreement_rate}`. (2) **Stabilité multi-runs** — `compute_multirun_stability(runs, reference=None)` mesure `pairwise_disagreement_mean/max` (Jaccard token-level), `identical_run_rate`, `n_distinct_outputs` ; si reference fournie, calcule `cer_per_run`, `cer_mean`, `cer_stdev`, `cer_cv` (None si mean=0 pour éviter division par zéro). Retourne None si <2 runs. Pure couche de calcul : pas d'extension du loader pour multi-GT, pas d'option runner `--repeats N`, pas de détecteur narratif `engine_unstable` — reportés à des sprints dédiés. +26 tests dans `test_sprint83_reliability.py` (cohen_kappa 6 cas dont accord parfait/désaccord pire que hasard κ=-1/un seul label, krippendorff 5 cas dont missing/single label corpus-wide, compute_iaa 5 cas dont empty/one-empty, multirun 6 cas dont reference parfaite et CV indéfini, _aligned_char_pairs 4 cas). **Verrou levé** : le rapport pourra demain afficher *« CER de Pero 4,2 % approche le plafond inter-paléographes κ=0,89 »* et signaler les pipelines LLM dont la variance dépasse un seuil. | | 82 | **Sprint 51 du plan d'évolution 2026 — A.I.9 : section « Leviers d'amélioration » (couche calcul + cards HTML)**. Le moteur narratif Sprint 19 dit *ce qui s'est passé* ; ce sprint dit *sur quelle dimension un effort éditorial pourrait porter* — purement factuel, jamais prescriptif. Nouveau module `picarones/measurements/levers.py` : dataclass `Lever(type, importance, payload, engines_involved)`, `LeverImportance` (HIGH=70/MEDIUM=40/LOW=10), registre via décorateur `@register_lever` (parallèle au registre narratif), `detect_levers(benchmark_data)` trie par importance décroissante. **5 détecteurs** : `dominant_recoverable_class` (≥30 % d'erreurs récupérables Sprint 77, HIGH si ≥50 %, top-3 classes), `pareto_concentration` (top-20 % des docs ≥50 % du CER cumulé sur le moteur leader, HIGH si ≥75 %), `complementarity_observation` (factuel sur `inter_engine_analysis.complementarity_gap` Sprint 35, HIGH si rel_gap ≥50 %), `lexical_modernization_observation` (top-3 tokens GT systématiquement modernisés Sprint 80, min_total=3, min_rate=0.50, HIGH si max_rate ≥90 %), `robustness_projection_observation` (déficit projeté ≥2 points de CER Sprint 81, HIGH si ≥5 points, sorted desc). Nouveau module `picarones/report/levers_render.py` : `build_levers_section_html` rend des **cards** server-side (étiquette i18n + phrase factuelle + détail compact + niveau d'importance coloré bleu/orange). Adaptive : `""` si aucun levier exploitable. Anti-injection systématique. Garde-fou anti-hallucination identique au moteur narratif : chaque chiffre rendu est dans le `payload` (test prouve la traçabilité FR+EN sur 3 leviers). +18 clés i18n FR/EN. +40 tests (modèle 3, dominant_recoverable 6, pareto 5, complementarity 4, lexical 4, robustness 4, pipeline 3, rendu 6, anti-hallucination 3, complétude i18n 2). **Verrou levé** : le rapport propose une lecture compacte des dimensions actionnables sans imposer de verdict — *« 65 % des erreurs de Tesseract sont récupérables », « 12 % des docs concentrent 78 % du CER », « top tokens modernisés : maistre, nostre, veoir »* — le chercheur juge selon son workflow. | | 81 | **Sprint 50 du plan d'évolution 2026 — A.I.8 : robustesse synthétique projetée sur corpus réel (couche calcul)**. `robustness.py` (Sprint 8) génère des courbes CER vs dégradation synthétique ; `image_quality.py` mesure le bruit/flou réels. Ce sprint projette les caractéristiques réelles sur les courbes pour estimer le déficit attendu. Nouveau module `picarones/measurements/robustness_projection.py` : `_interpolate_cer(levels, cer_values, target_level)` interpolation linéaire avec clip aux bornes (pas d'extrapolation hasardeuse), filtre cer None ; `_extract_quality_value(quality_dict, degradation_type, custom_mapping)` extrait depuis ImageQualityResult (mapping default noise→noise_level, blur→blur_score, etc.) ; `project_robustness_on_corpus(curves, image_qualities)` retourne `{engine: {deg_type: {n_docs, n_docs_with_data, expected_cer_mean/median, baseline_cer, deficit_vs_baseline, n_docs_above_critical, critical_threshold}}}` ; `aggregate_projection_per_engine` somme les déficits par moteur et identifie le worst_degradation_type (hypothèse d'indépendance documentée). +22 tests (interpolation 7 cas, extraction 4 cas, projection 7 cas, agrégation 4 cas). **Verrou levé** : un bench BnF lit « 30 % de vos documents ont un bruit où Tesseract perd 8 points — déficit attendu 2,4 points » — la courbe de robustesse n'est plus déconnectée du corpus réel. | | 80 | **Sprint 49 du plan d'évolution 2026 — A.I.7 : sur-normalisation lexicale (couche calcul + table HTML)**. Le détecteur `llm_hallucination_flag` (Sprint 19) signale via un score agrégé mais ne dit pas **quoi** corriger dans le prompt. Nouveau module `picarones/measurements/lexical_modernization.py` : `compute_lexical_modernization(reference, hypothesis, stop_list, case_sensitive)` aligne mot-à-mot via `difflib.SequenceMatcher` et accumule par token GT `{n_total, n_modernized, rate_modernized, variants}` ; `aggregate_lexical_modernization` somme corpus-wide ; `top_modernized_tokens(data, n=20, min_total=1)` retourne les N tokens GT les plus modernisés (tri décroissant par taux, tie-break par n_total, filtre anecdotiques via min_total). Stop-list paramétrable (par défaut vide). Suppression GT → variant ∅. Nouveau module `picarones/report/lexical_modernization_render.py` : `build_lexical_modernization_html(data, labels, top_n, min_total)` tableau 4 colonnes (forme GT, variantes OCR top-3, n GT, % modernisé gradient blanc→orange). Adaptive : "" si data None ou aucun modernisé. +6 clés i18n FR/EN. +20 tests (calcul 9 cas dont systématique/préservé/partiel/multi-variants/stop-list/casse/suppression/vide, agrégation 2 cas, top 2 cas, rendu 5 cas dont anti-injection, complétude i18n). **Verrou levé** : le chercheur lit « maistre → maître modernisé dans 100 % des cas » et ajuste son prompt — info exploitable au lieu d'un score agrégé. | | 79 | **Sprint 48 du plan d'évolution 2026 — A.I.6 : projection de coût en volume cible (couche de calcul)**. La vue Pareto (Sprint 20) trace CER vs coût mais le coût est par unité (1 000 pages) ; payer 50 € de plus sur 50 pages est trivial, sur 5 millions ça change tout. Nouveau module `picarones/measurements/cost_projection.py` : `ProjectedCost(engine_key, target_pages, cost_total_eur, co2_total_g, cost_per_1k_pages_eur, co2_per_1k_pages_g, type)`, `project_cost_total/co2_total` linéaire en pages avec `None` si données insuffisantes ou target<0, `project_engine` retourne le ProjectedCost complet, `project_all_engines(engine_costs, target_pages)` projette N moteurs (ValueError si target<0, moteurs sans données conservés avec cost_total=None), `cost_gap_table(projections, baseline)` retourne `{engine: {total, delta_abs, delta_rel}}` vs baseline (KeyError si baseline inconnue, delta_rel=None si baseline=0). +17 tests (calcul 5 cas, CO₂ 2 cas, engine 2 cas, all_engines 3 cas, gap_table 4 cas, **cas réaliste BnF 80 000 pages BMS** Tesseract=3.20€/Pero=0€/Mistral=280€/GPT-4o=600€). **Verrou levé** : couche calcul prête pour câbler le panneau « Avancé » avec champ « Volume cible » qui recalcule Pareto et table coût en valeur totale projetée. UX HTML suivra. | | 78 | **Sprint 47 du plan d'évolution 2026 — A.I.5 : équivalences diplomatiques en curseur fin (couche de calcul)**. Les profils `DIPLOMATIC_*` de `normalization.py` appliquent un bloc entier ; un éditeur peut vouloir nuancer (« je tolère ſ→s mais pas u→v »). Nouveau module `picarones/measurements/equivalence_profile.py` : dataclass `EquivalenceRule(name, source, target, description, profile_tag)`, catalogue `BUILTIN_EQUIVALENCES` dérivé automatiquement des 4 profils intégrés avec 15 règles canoniques nommées (`longs_s`, `u_eq_v`, `i_eq_j`, `ae_ligature`, `thorn_th`, `vv_eq_w`, etc.), `list_equivalences_by_profile`, `apply_selected_equivalences(text, selected_names)` (règles inconnues ignorées + warning, texte vide → ""), `compute_cer_with_equivalences(reference, hypothesis, selected_names)` qui applique les deux côtés puis renvoie CER. Aucune modification de normalization.py — purement additif. +17 tests (catalogue 4 cas, liste 3 cas, apply 6 cas dont sélectif/exclu/multi/inconnue, compute_cer 4 cas dont application bilatérale). **Verrou levé** : la couche calcul est prête pour câbler le panneau « Avancé » du rapport avec cases à cocher granulaires et recalcul JS client. UX (URL state + debounce) suivra dans un sprint dédié. | | 77 | **Sprint 46 du plan d'évolution 2026 — A.I.4 chantier 3 : taxonomie comparative côte-à-côte (clôture A.I.4)**. Troisième chantier d'A.I.4. Répond à « deux moteurs ont le même CER global, mais lequel fait des erreurs plus récupérables ? ». Nouveau module `picarones/measurements/taxonomy_comparison.py` : `compare_taxonomies(engine_a, counts_a, engine_b, counts_b)` normalise en proportions, calcule deltas signés, agrège par niveau de **récupérabilité éditoriale** (recoverable: case/ligature/abbreviation ; difficult: diacritic/visual/hapax ; irrecoverable: lacuna/oov/segmentation). Constante `RECOVERABILITY` exportée. Retourne None si vide. Nouveau module `picarones/report/taxonomy_comparison_render.py` : `build_taxonomy_comparison_html` produit titre + note + diagramme miroir SVG + tableau résumé par catégorie. `_build_mirror_chart_svg` server-side : ligne par classe, barres horizontales A à gauche / B à droite, étiquette au centre, %, couleur selon récupérabilité (vert/orange/rouge), échelle normalisée. `_build_recoverability_summary_html` : tableau 3×2 avec pastilles colorées. Adaptive : "" si None ou pas de classes. +6 clés i18n FR/EN. +18 tests (calcul 7 cas dont sanité RECOVERABILITY couvre ERROR_CLASSES, rendu 7 cas, anti-injection, i18n). **Choix éditorial assumé** : classification recoverable/difficult/irrecoverable est un guide pragmatique, pas un verdict — note explicative dit « à CER égal, un moteur dont les erreurs sont majoritairement vertes est préférable pour une édition critique ». **A.I.4 livré bout-en-bout** (Sprints 75-77). | | 76 | **Sprint 45 du plan d'évolution 2026 — A.I.4 chantier 2 : évolution intra-document des classes taxonomiques (couche calcul + heatmap SVG)**. `line_metrics.py` (Sprint 10) avait déjà heatmap CER×position ; ce sprint l'étend à toutes les classes taxonomiques. Nouveau module `picarones/measurements/taxonomy_intra_doc.py` : `compute_taxonomy_position_heatmap(reference, hypothesis, n_bins=10)` calcule par classe le compte par tranche de position, réutilise classification mot-à-mot Sprint 5 en gardant `i1` (position GT) et binnifiant via `floor(i1/n_gt*n_bins)`. `_classify_word_pair` variante pure. `_bin_for_position` clip 0..n_bins-1. `ValueError` si n_bins≤0, `None` si GT vide. Nouveau module `picarones/report/taxonomy_intra_doc_render.py` : `build_taxonomy_intra_doc_html` produit heatmap SVG class×position avec gradient blanc→orange profond, densité relative au max de chaque classe (met en évidence les positions concentrées), filtrage classes avec ≥1 erreur, étiquettes positions/classes, accessible. Adaptive : `""` si None/no_errors/aucune classe avec erreurs. +3 clés i18n FR/EN. +16 tests (calcul 8 cas dont identité/début/fin/uniforme/breakdown, rendu 5 cas, anti-injection, complétude i18n). **Verrou levé** : un chercheur voit où chaque type d'erreur apparaît — distingue erreurs de marge (concentrées) vs scribe (uniformes). | | 75 | **Sprint 44 du plan d'évolution 2026 — A.I.4 chantier 1 : co-occurrence taxonomique (couche calcul + heatmap SVG)**. Premier des 3 chantiers d'A.I.4. Répond à « quelles classes d'erreur tendent à apparaître ensemble ? » — utile pour stratifier *a posteriori*. Nouveau module `picarones/measurements/taxonomy_cooccurrence.py` : `compute_taxonomy_cooccurrence(per_doc_classes, min_doc_count=1, top_n_pairs=10)` calcule l'indice de Jaccard entre paires de classes au niveau document (présence binaire), symétrique, diagonale=1.0, filtrage classes anecdotiques via min_doc_count, top_pairs triées Jaccard décroissant. Retourne None si vide. Nouveau module `picarones/report/taxonomy_cooccurrence_render.py` : `build_taxonomy_cooccurrence_html` produit titre + note + heatmap SVG + table top_pairs. `_build_heatmap_svg` server-side avec cellules colorées blanc→bleu profond, valeur affichée si >0.05, étiquettes rotées -45° en haut/normales à gauche, accessible (role/aria-label). Adaptive : "" si None ou matrice vide. +5 clés i18n FR/EN. +22 tests (calcul 11 cas dont toujours/jamais ensemble, diagonale, symétrie, chevauchement partiel, min_doc_count, top_pairs triées, none doc skipped ; rendu 7 cas ; anti-injection ; complétude i18n). **Verrou levé** : un chercheur voit d'un coup d'œil quelles classes d'erreur sont corrélées dans son corpus. | | 74 | **Sprint 43 du plan d'évolution 2026 — A.I.3 chantier 1 : encart HTML « Ce corpus est-il habituel ? » (clôture A.I.3)**. Suite directe Sprint 73 (couche calcul + détecteur narratif). Nouveau module `picarones/report/baseline_render.py` : `build_corpus_difficulty_baseline_html(percentile_data, historical_values, labels)` produit phrase factuelle + boxplot SVG, phrase template auto-sélectionnée selon harder_than_usual/easier_than_usual/usual flags. `_build_difficulty_boxplot_svg` server-side avec moustache min→max, boîte Q1→Q3, médiane, point courant **coloré adaptive** (bleu si Q3 plus difficile, vert sinon habituel), étiquettes numériques, accessible (role/aria-label). Helper `_quantiles` méthode inclusive gère N=0/1. Adaptive : `""` si percentile_data None, boxplot omis si historical_values vide. +4 clés i18n FR/EN avec templates Python `{current:.2f}/{percentile:.0f}/{n_runs}`. +20 tests (quantiles 3 cas, SVG 8 cas dont couleurs/dégénéré, HTML 6 cas, anti-injection, complétude i18n). **Verrou levé** : un bench avec historique SQLite chargé voit en tête de rapport « ce corpus est plus difficile que la moyenne — au 88ᵉ percentile des 47 corpus précédents » avec boxplot. **A.I.3 livré bout-en-bout** (Sprint 73 calc+narrative + Sprint 74 vue HTML). | | 73 | **Sprint 42 du plan d'évolution 2026 — A.I.3 chantier 2 : détecteur narratif `engine_off_baseline` (couche calcul + narrative)**. L'historique SQLite (Sprint 8) existait mais aucun détecteur narratif ne le lisait. Répond à « comment ce moteur se comporte-t-il sur ce corpus par rapport à ses runs précédents de mon institution ? ». L'encart HTML « Ce corpus est-il habituel ? » (chantier 1, boxplot SVG) suit Sprint 74. Nouveau module `picarones/measurements/baseline_comparison.py` : `compute_engine_baseline(history, engine_name, corpus_name, current_cer, current_run_id, min_runs=5, relative_delta_threshold=0.20)` filtre apple-to-apple par moteur×corpus, exclut le run courant si fourni, retourne dict avec cer_current/historical_mean/median, n_runs, absolute_delta, relative_delta, off_baseline ; `compute_corpus_difficulty_percentile` place la difficulté courante dans la distribution historique (lit metadata.difficulty), flags harder/easier_than_usual (P75/P25). Nouveau `FactType.ENGINE_OFF_BASELINE` + détecteur `detect_engine_off_baseline` (priority 150) qui émet 1 Fact par moteur off_baseline, importance HIGH si |delta|≥50% sinon MEDIUM, silencieux si baseline_comparisons absent/vide ou relative_delta=None. Templates FR/EN. +21 tests : couche calcul (9 cas dont min_runs/current_run_id/baseline=0/CER None), percentile (4 cas), détecteur (6 cas), **traçabilité anti-hallucination FR+EN** (chaque nombre rendu traçable au payload). **Verrou levé** : un bench BnF qui pousse ses résultats dans l'historique voit dans la synthèse « ce moteur a un CER inhabituel sur ce corpus par rapport à vos 12 runs précédents ». | | 72 | **Sprint 41 du plan d'évolution 2026 — A.I.1 chantier 1 : vue HTML « Worst lines globale » (clôture A.I.1)**. Suite directe Sprint 71 — la métrique rare-token recall est livrée, ce sprint livre la vue qui transcende les documents pour exposer les lignes individuelles les plus mal transcrites du corpus. Nouveau module `picarones/measurements/worst_lines.py` : dataclass `WorstLineEntry(rank, cer, engine_name, doc_id, line_index, gt_line, hyp_line, script_type)`, `extract_worst_lines(benchmark, top_n=20, engine_filter, script_type_filter)` collecte transversalement à tous les moteurs et docs, filtre par moteur et par strate (Sprint 45 doc_strata), trie par CER décroissant, retourne top_n avec rang 1-based. Récupère les textes GT/hyp par re-split du DocumentResult à l'index de ligne (limite : suppose BenchmarkResult non-compacté). Lignes CER=0 ignorées. Nouveau module `picarones/report/worst_lines_render.py` : `build_worst_lines_table_html(entries, labels)` server-side avec colonnes Rang/CER (gradient jaune→rouge)/Moteur/Doc/Ligne#/[Strate]/Diff GT→OCR. Colonne strate **adaptive** (omise si aucune entry n'en a). Diff caractère par caractère via `diff_utils.compute_char_diff` (Sprint 5), rouge barré pour suppressions, vert pour insertions. Anti-injection systématique. Retourne `""` si vide. +25 tests (extraction 5 cas, filtres 4 cas, edge cases 4 cas — pas de line_metrics, vide, sans doc_strata, hyp plus courte —, rendu 8 cas, anti-injection 4 cas). **Verrou levé** : un chercheur qui voit `5% de mes lignes ont un CER > 0.42` dans le rapport peut désormais voir **quelles** lignes — diff inline, document parent, ligne#, moteur — pour comprendre ce qui casse. | | 71 | **Sprint 40 du plan d'évolution 2026 — A.I.1 chantier 2 : rare-token recall (couche de calcul, démarrage de la résolution des critiques structurelles A.I)**. Premier sprint A.I qui s'attaque à la critique « la granularité ne s'arrête plus à la page ». Mesure le rappel sur les tokens rares (hapax + dis legomena, défaut `max_freq=2`) — répond à *« ce moteur préserve-t-il les noms propres rares qui m'intéressent pour l'indexation prosopographique ? »*. Nouveau module `picarones/measurements/rare_tokens.py` : `tokenize` Unicode-aware (contractions `L'an`/`d’une`, composés `peut-être`, apostrophe typographique `’` U+2019), `frequency_distribution(documents, case_sensitive)` → `{token: count}` corpus-wide, `extract_rare_tokens(documents, max_freq=2)` → `frozenset`, `compute_rare_token_recall(reference, hypothesis, rare_tokens)` retourne `{n_rare_tokens_in_reference, n_rare_tokens_recalled, recall, missed_tokens}` avec alignement bag-of-tokens multiplicitaire. **Pas d'enregistrement registre typé** (la métrique exige un 3ᵉ argument set des rares, calculé corpus-wide). +28 tests (tokenisation 8 cas, frequency 4 cas, extraction 4 cas, recall 10 cas avec multiplicité/casse/dégénérés, raccourci, **test propriété cas réaliste registre état civil** prouvant que rare-token recall discrimine plus que CER quand l'OCR rate les noms propres). **Verrou levé** : un bench BnF qui veut savoir « ce moteur préserve-t-il bien les noms de famille ? » a maintenant la métrique adaptée. Vue HTML « Worst lines + tokens rares manqués » suit Sprint 72 (chantier 1 d'A.I.1). | | 70 | **Sprint 39 du plan d'évolution 2026 — Étape 4 / axe B : CLI pour piloter les pipelines composées sans Python**. Permet de spécifier une pipeline ou une comparaison de N pipelines dans un YAML déclaratif et de les exécuter via la CLI, sans écrire de Python. Nouveau module `picarones/measurements/pipeline_spec_loader.py` : `load_pipeline_spec_from_yaml/dict` parse YAML → `PipelineSpec` (steps avec dotted path module, args kwargs, inputs_from optionnel pour DAG branchant), `load_comparison_specs_from_yaml` retourne `(specs, extras)` pour comparaison. Import dynamique via `importlib`, validation stricte que la classe hérite de `BaseModule`. Exception `PipelineSpecLoadError` avec messages explicites pour 8 cas d'erreur. Nouveau sous-groupe CLI `picarones pipeline` : `run --corpus ` (avec --output-json/--output-html/--lang) et `compare --corpus ` (avec --output-html/--baseline). Le CLI lit `rankings` du YAML pour configurer la vue HTML comparative. **Aucun module métier ajouté** : le YAML référence des classes tierces que l'utilisateur a installées. +27 tests (resolve_class 5 cas, load_from_dict 9 cas, load_from_yaml 3 cas, load_comparison 2, CLI run 2, CLI compare 2, CLI help 3). **Verrou levé** : workflow BnF type — `picarones pipeline run my_pipeline.yaml --corpus ./scans --output-html rapport.html` — sans ingénieur Python dans la boucle. Spec versionnable en git pour la reproductibilité. | | 69 | **Sprint 38 du plan d'évolution 2026 — Étape 4 / axe B : documentation utilisateur « Écrire un module pour le banc d'essai de pipelines »**. Premier guide pédagogique dédié à l'axe B. Nouveau document `docs/user/writing-a-pipeline-module.md` couvrant bout-en-bout : TL;DR avec exemple `MyCorrector` minimal, contrat `BaseModule` (tableau des champs + liste des `ArtifactType`), 3 exemples mockés explicitement étiquetés « pédagogique » (correcteur LLM TEXT→TEXT, reconstructeur TEXT→ALTO, classifieur TEXT→ENTITIES), orchestration mono-doc/corpus/comparaison/DAG branchant avec snippets exécutables (Sprints 63-66), génération de rapport HTML autonome (Sprints 67-68), bonnes pratiques (discipline des types, erreurs gracieuses, **pas de seuils éditoriaux dans votre module**), anti-patterns FAQ (« pourquoi pas de correcteur LLM intégré ? »…), tableau de référence rapide des sprints axe B. +34 tests anti-régression dans `test_sprint69_user_doc.py` (7 sections principales, 15 concepts API mentionnés, philosophie « banc d'essai pas atelier » + « aucun module métier » présente, références aux 6 sprints axe B + phase 0, ≥ 5 blocs Python + imports valides). **Verrou levé** : la barrière d'entrée pour un utilisateur tiers passe de « lire le code source des 6 sprints » à « lire un guide d'une page avec snippets copier-coller ». | | 68 | **Sprint 37 du plan d'évolution 2026 — Étape 4 / axe B : vue HTML de comparaison de N pipelines composées**. Suite directe Sprint 67 — la vue mono-pipeline est étendue avec un rendu comparatif entre N pipelines exécutées sur le même corpus (Sprint 65). Extension de `pipeline_render.py` : `RankingSpec(artifact_type, metric_name, higher_is_better=False, label=None)` (dataclass avec `display_label` auto/explicite), `build_pipeline_ranking_table_html(comparison, ranking_spec)` (tableau rang×pipeline×valeur, cellule rang colorée gradient vert→rouge, pipelines sans valeur en queue), `build_pipeline_gain_table_html(comparison, ranking_spec, baseline_pipeline)` (tableau pipeline×{valeur, abs, rel} vs baseline, cellule colorée vert favorable/rouge défavorable selon `higher_is_better`, baseline marquée), `build_pipeline_comparison_summary_html(comparison)` (corpus + counts + mini-résumé par pipeline), `build_pipeline_comparison_report_html(comparison, ranking_specs, baseline_pipeline, lang)` (document HTML autonome). **Pas d'auto-détection magique** : l'utilisateur déclare explicitement les `ranking_specs` à afficher et la baseline. +14 clés i18n FR/EN. +26 tests dans `test_sprint68_pipeline_comparison_html.py` (RankingSpec, ranking table avec ordre/queue/couleur, gain table avec baseline/couleurs/baseline inconnue, summary, document autonome avec rankings conditionnels, anti-injection, complétude i18n). **Pas de classification automatique imposée**. **Verrou levé** : `build_pipeline_comparison_report_html(comparison, ranking_specs=[RankingSpec(TEXT, "cer", label="CER")], baseline_pipeline="ocr_only")` produit en une ligne un rapport HTML autonome avec ranking + gain table. | | 67 | **Sprint 36 du plan d'évolution 2026 — Étape 4 / axe B : vue HTML d'un benchmark de pipeline composée (rapport autonome)**. Pattern identique aux Sprints 41/43/62 : rendu server-side, pas de JS, anti-injection systématique. Nouveau module `picarones/report/pipeline_render.py` avec 3 fonctions : `build_pipeline_summary_html(bench)` (encart corpus-wide avec cellule colorée par taux de succès et durée formatée), `build_pipeline_steps_table_html(bench)` (tableau par étape avec 8 colonnes : nom, succeeded/failed, taux succès gradient, durée mean/median, métriques aux jonctions `.: mean (n=N)`, error_breakdown catégorisé), `build_pipeline_report_html(bench, lang)` (document HTML autonome `` + styles CSS inline + attribut `lang` FR/EN). **Rapport distinct du générateur OCR historique** : le `ReportGenerator` attend `BenchmarkResult` (axe A), pour les pipelines on a `PipelineBenchmarkResult` (axe B). Pas de couplage, on livre un document autonome. +18 clés i18n FR/EN. +21 tests dans `test_sprint67_pipeline_html.py` (summary, steps table 8 colonnes, document autonome avec doctype/head/body/styles, anti-injection sur pipeline/corpus/step/labels, complétude i18n). **Pas de classification automatique** : chiffres bruts uniquement. **Verrou levé** : `Path("rapport.html").write_text(build_pipeline_report_html(bench))` produit directement un rapport autonome après `run_pipeline_benchmark`. **Reporté Sprint 68** : rendu d'un `PipelineComparisonResult` (ranking + gain table). | | 66 | **Sprint 35 du plan d'évolution 2026 — Étape 4 / axe B : DAG branchant via `inputs_from`**. Les Sprints 63-65 traitaient des pipelines séquentielles (le bag d'artefacts garde une seule version par type, la plus récente écrase la précédente). Sprint 66 permet de **désigner explicitement la source d'un artefact** quand plusieurs étapes produisent le même type, débloquant fork/merge dans une même pipeline. `PipelineStep.inputs_from: dict[ArtifactType, str]` (vide par défaut) où `str` = nom d'une étape antérieure ou `"__initial__"` pour les entrées initiales. **Bag versionné** dans le runner : `versioned[(type, source_step_name)] = artifact` + `latest[type] = step_name` ; en l'absence d'`inputs_from`, on prend la dernière version (rétrocompat stricte Sprint 63). **Validation étendue** dans `PipelineSpec.validate` : détecte référence vers étape inconnue, étape qui ne produit pas le type, type non consommé par le module. **Référence vers étape échouée** : l'étape en aval rapporte `entrée manquante : @` (marqueur `@step` qui dit "dépendance vers step en échec, pas type absent"). +11 tests dans `test_sprint66_dag_branching.py` (défauts, validation 4 cas, fork explicite avec métriques indépendantes, **test propriété fork vs chain divergent** prouvant que `inputs_from` change le résultat, référence vers step échouée, rétrocompat). Les 42 tests Sprints 63-65 passent sans modification. **Verrou levé** : composer une pipeline qui fork un OCR vers N branches de correction et évaluer chacune indépendamment dans une seule spec, sans basculer sur `compare_pipelines`. | | 65 | **Sprint 34 du plan d'évolution 2026 — Étape 4 / axe B : comparaison de N pipelines composées sur le même corpus**. Suite directe Sprints 63-64. Répond à la question BnF « OCR seul vs OCR+correcteur A vs OCR+correcteur B : laquelle gagne et de combien ? ». Philosophie inchangée (banc d'essai). Nouveau module `picarones/measurements/pipeline_comparison.py` : `compare_pipelines(specs, corpus, factories=None)` exécute N `PipelineSpec` sur le **même** corpus (apple-to-apple), garde-fou noms uniques (sinon `ValueError`), `factories` optionnel pour personnaliser les entrées initiales par pipeline. `PipelineComparisonResult(corpus_name, n_docs, per_pipeline: dict[name → PipelineBenchmarkResult], total_duration_seconds)` avec `pipeline_names()` qui préserve l'ordre, `ranking_by_final_metric(artifact_type, metric_name, higher_is_better)` qui retourne `[(name, mean)]` trié (pipelines sans métrique en queue), `gain_table(artifact_type, metric_name, baseline_pipeline)` qui retourne `{name: {value, absolute, relative}}` (relative à None si baseline=0, KeyError si baseline inconnue). Pure infrastructure, aucun module métier ajouté — on réutilise `run_pipeline_benchmark` Sprint 64 et on ajoute la couche comparative. +13 tests dans `test_sprint65_pipeline_comparison.py` (single/multi/ordre/duplicate/empty, ranking ascendant/descendant et sans métrique, gain_table avec baseline inconnue/self/zero, cas réaliste OCR+fixer outperforms baseline, factories par pipeline, dataclass). **Verrou levé** : comparaison de N pipelines tierces apple-to-apple. Vue HTML dédiée et tests statistiques inter-pipelines arrivent ensuite. | | 64 | **Sprint 33 du plan d'évolution 2026 — Étape 4 / axe B : orchestration corpus-wide d'une pipeline composée**. Suite directe Sprint 63 — le `PipelineRunner` Sprint 63 exécute mono-document ; ce sprint fournit l'orchestration sur un **corpus complet** et l'agrégation par étape. Philosophie inchangée (banc d'essai, pas atelier). Nouveau module `picarones/measurements/pipeline_benchmark.py` : `InitialInputsFactory` (callable `Document → dict[ArtifactType, Any]`), `default_initial_inputs(doc)` (factory par défaut `{IMAGE: doc.image_path}`), `StepAggregate(step_name, n_docs, n_succeeded, n_failed, duration_seconds_total/mean/median, failing_doc_ids, junction_metrics, error_breakdown)` qui agrège par étape avec métriques numériques mean/median/n par type d'artefact et catégorisation des erreurs (`missing_input`/`raised_exception`/`missing_output`/`pipeline_aborted`/`other`), `PipelineBenchmarkResult(pipeline_name, corpus_name, n_docs, per_doc_results, per_step_aggregates, total_duration_seconds)` avec `n_pipelines_succeeded`/`n_pipelines_failed` et `aggregate_for_step(name)`, `run_pipeline_benchmark(spec, corpus, factory)` qui itère séquentiellement, capture gracieusement les erreurs de la factory et propage les erreurs de spec à tous les docs. Périmètre : séquentiel inter-documents ; comparaison N pipelines (Sprint 65), DAG branchant (Sprint 66), vue HTML (Sprint 67), parallélisation à arbitrer. +13 tests dans `test_sprint64_pipeline_benchmark.py` (factory par défaut, corpus vide, 1 doc OK, métriques agrégées sur 3 docs, mix succès/échecs avec failing_doc_ids et error_breakdown catégorisé, 2 étapes avec rebond propre où l'étape 2 reçoit `missing_input`, spec invalide → tous en `pipeline_aborted`, factory personnalisée, factory qui lève sur un doc seulement, dataclasses). **Tous les modules utilisés sont des mocks** (MockOCR, MockCrasherSometimes, MockTextRewriter) — Picarones n'expose aucun module métier. **Verrou levé** : un utilisateur peut maintenant lancer une pipeline composée tierce sur tout son corpus, obtenir l'agrégat par étape (durée mean/median, métriques mean/median, taux d'erreur par catégorie) et les résultats par document. | | 63 | **Sprint 32 du plan d'évolution 2026 — Étape 4 / axe B : banc d'essai de pipelines composées (couche d'orchestration mono-document)**. Démarrage de l'axe B du plan 2026 — Picarones reste un **banc d'essai**, pas un atelier de production : ce sprint livre l'infrastructure qui permet d'**évaluer des pipelines composées de modules tiers** que l'utilisateur amène (ses propres `BaseModule` Sprint 33), **sans qu'aucun module métier ne soit fourni par Picarones**. Nouveau module `picarones/core/pipeline.py` : `PipelineStep(name, module)` (lit les `input_types`/`output_types` du module), `PipelineSpec(name, steps)` (DAG séquentiel + `validate()`/`is_valid()` qui vérifie statiquement que les types s'enchaînent), `StepResult` (durée, output_types, junction_metrics, error), `PipelineResult` (succeeded, failing_steps, `junction_metrics_for(artifact_type)` qui ignore les étapes en erreur), `PipelineRunner.run(spec, document, initial_inputs)` qui exécute mono-document, valide les entrées disponibles, chronomètre chaque étape en wall-clock, capture gracieusement les exceptions, valide que les sorties déclarées sont produites, et **évalue automatiquement chaque type produit contre la GT du même niveau** (Sprint 32) via `compute_at_junction` (Sprint 34). Eager-load au top du module des registres de métriques (`builtin_metrics` + 6 philologiques + NER/reading_order/readability) pour garantir que `compute_at_junction` ait accès à toutes les métriques sans import explicite par l'utilisateur. **Périmètre Sprint 63** : séquentiel mono-document ; DAG branchant, parallélisation, agrégation corpus-wide et vue HTML dédiée reportés à des sprints suivants de l'axe B. +16 tests dans `test_sprint63_pipeline_runner.py` (validation de spec, exécution 1 étape parfaite/imparfaite, 2 étapes chaînées avec CER qui baisse après correction par le rewriter, erreurs gracieuses sur 3 cas — module qui lève / module silencieux / spec invalide —, pas de GT → pas de métriques sans erreur, mesure du temps, dataclasses, `junction_metrics_for` qui skippe les étapes en erreur). **Tous les modules utilisés sont des mocks définis dans le fichier de test** (MockOCR, MockTextRewriter, MockCrasher, MockSilentDropper) — Picarones n'expose volontairement aucun module métier. **Verrou levé** : l'utilisateur peut désormais brancher ses propres modules tiers (correcteur LLM, reconstructeur ALTO, re-segmenteur, classifieur d'entités), composer une pipeline et obtenir automatiquement les métriques à chaque étape contre la GT correspondante. | | 62 | **Sprint 31 du plan d'évolution 2026 — Étape 3 / vue HTML « Profil philologique » (clôture câblage philologique bout-en-bout)**. Suite directe Sprint 61 (câblage backend) — produit le bloc HTML qui remonte les six modules philologiques (Sprints 55-60) dans le rapport. Pattern identique aux Sprints 41 (NER) et 43 (calibration) : rendu server-side, pas de JS, déterministe. Nouveau module `picarones/report/philological_render.py` : 6 fonctions de rendu de section (`build_unicode_blocks_section`, `build_abbreviations_section`, `build_mufi_section`, `build_early_modern_section`, `build_modern_archives_section`, `build_roman_numerals_section`) + agrégateur `build_philological_profile_html` qui assemble en un bloc unique avec note explicite « L'outil ne classifie pas la convention adoptée par chaque moteur — c'est au chercheur de lire les chiffres et de conclure selon ses critères éditoriaux ». **Adaptive masking complet** : chaque section conditionnée à la présence de signal sur ≥ 1 moteur ; agrégateur retourne `""` si aucun signal global. Cellules colorées par gradient rouge→vert proportionnel au score (sémantique inversée pour `lost` des numéraux : haut taux = rouge). Effectifs `n=…` affichés à côté de chaque score. Câblage `ReportGenerator.generate` + `view_analyses.html` (chart-card pleine largeur conditionné). Anti-injection HTML systématique via `html.escape`. **Aucune classification automatique** : `diplomatique`/`modernisant` n'apparaît que dans la note d'usage, jamais accolé à un moteur. +25 clés i18n FR/EN (`philo_profile_*`, `philo_unicode_*`, `philo_abbreviations_*`, `philo_mufi_*`, `philo_early_modern_*`, `philo_modern_archives_*`, `philo_roman_numerals_*`, `philo_roman_status_*`). +18 tests dans `test_sprint62_philological_html.py` (sections ×6, adaptive masking, anti-injection sur nom moteur + libellé i18n, %, code couleur, pas de classification imposée, complétude i18n). **Verrou levé** : les six modules philologiques sont livrés bout-en-bout (calcul Sprints 55-60 + backend Sprint 61 + HTML Sprint 62). Un benchmark sur n'importe quel fonds patrimonial européen produit automatiquement, sans configuration, un profil philologique lisible dans le rapport — donné par catégorie/bloc/statut, sans verdict. | | 61 | **Sprint 30 du plan d'évolution 2026 — Étape 3 / câblage backend des métriques philologiques au runner (Sprints 55-60)**. Suite directe Sprints 55-60. Les six modules philologiques sont désormais calculés automatiquement par le runner pour chaque document et agrégés par moteur, sans aucune option à activer. Nouveau module `picarones/measurements/philological_hooks.py` : `compute_philological_metrics(reference, hypothesis)` calcule les six modules avec **adaptive masking** (un module n'apparaît que si la GT a du signal exploitable : `n_markers_reference > 0`, `n_mufi_chars_reference > 0`, au moins un caractère hors Basic Latin pour unicode_blocks…) ; `aggregate_philological_metrics(per_doc_list)` agrège les compteurs bruts par module (somme), recalcule les scores globaux, et préserve les structures `per_block`/`per_abbreviation`/`per_char`/`per_category`/`per_status` agrégées. Nouveaux champs `DocumentResult.philological_metrics` et `EngineReport.aggregated_philological` (`Optional[dict]`, sérialisés conditionnellement, libérés par `compact`). Câblage runner : calcul inconditionnel (coût O(N) sur texte, négligeable face à l'OCR), erreur d'un module individuel n'arrête pas les autres + warning explicite. Rétrocompat stricte : aucun paramètre ajouté, comportement existant inchangé, un benchmark sans signal philologique n'a aucun champ ajouté au JSON. +24 tests dans `test_sprint61_philological_runner.py` (champs, sérialisation/compact, calcul adaptive sur 6 cas — médiéval/imprimé/moderne/romain/diacritiques/ASCII pur, agrégation des compteurs et recalcul des scores globaux, intégration runner end-to-end avec mock). **Verrou levé** : les six modules philologiques sont désormais visibles dans le pipeline standard de bench, il manque la vue HTML dédiée (Sprint 62). | | 60 | **Sprint 29 du plan d'évolution 2026 — Étape 3 / extension philologique transversale : numéraux romains (couche de calcul, clôture extension par période)**. Suite directe Sprints 56-59. Les numéraux romains traversent les trois périodes patrimoniales — médiéval (minuscules + j final `mcclxxxij`=1282), imprimé ancien (`Tome IV`), moderne (`Louis XIV`, `MCMXIV`). Module `picarones/measurements/roman_numerals.py` : `roman_to_int` parsing tolérant casse + j médiéval avec validation stricte des paires soustractives canoniques (IV, IX, XL, XC, CD, CM seulement — rejette `ICI`, `IL`, `VV`, `IIIII`), forme additive médiévale `IIII` acceptée, `int_to_roman` canonique, `detect_roman_numerals(text, min_length=1)` avec filtre paramétrable contre les single-letter ambigus (`I` pronom). `compute_roman_numeral_metrics` classifie chaque numéral GT en **5 statuts ordonnés par priorité** : `strict_preserved` (forme exacte), `case_changed` (valeur OK casse différente), `j_dropped` (j médiéval normalisé en i), `converted_to_arabic` (XIV→14), `lost`. Retourne `per_status`, `per_numeral`, `lost_numerals`, `global_strict_score`, `global_value_score` (toute forme préservant la valeur). `roman_numeral_strict_score` et `roman_numeral_value_score` enregistrés dans le registre typé Sprint 34 pour `(TEXT, TEXT)`. **Choix éditorial assumé identique aux Sprints 58-59** : pas de classification automatique — le chercheur lit `per_status` et juge la convention. +93 tests (parsing paramétrée standard + minuscules + j médiéval, formes invalides rejetées, aller-retour, détection avec min_length et frontière de mot anti-`VIVE`, **rejet du faux positif `ICI`**, 5 statuts individuellement, priorité strict>arabic, **3 cas réalistes par période** — charte médiévale, imprimé ancien, souverain moderne —, comptage exhaustif somme des per_status = total, dégénérés, raccourcis, intégration registre). **Verrou levé** : l'extension philologique transversale est intégralement livrée — un benchmark sur n'importe quel fonds patrimonial européen peut désormais classer les moteurs sur leur traitement des numéraux romains, indépendamment de la période. | | 59 | **Sprint 28 du plan d'évolution 2026 — Étape 3 / extension philologique aux périodes contemporaines : marqueurs et abréviations des archives modernes XIXᵉ-XXᵉ (couche de calcul)**. Suite directe Sprints 56-58. Sur les fonds modernes BnF (état civil, recensements, presse, monographies, archives militaires, annuaires) la typographie historique a disparu mais subsiste un riche système d'abréviations contemporaines. Module `picarones/measurements/modern_archives.py` avec **9 catégories** : `civility_titles` (Mme, Mlle, Mgr, Dr, Pr, Me, M., R.P., S.M., S.A.R., S.E., S.S.), `ordinals` (1ᵉʳ, 1ʳᵉ, 2ᵈ, 2ᵉ, Vᵉ, XIᵉ-XXᵉ avec exposants Unicode), `currency` (₶, ₣, ƒ, £ + l./s./d. d'Ancien Régime), `administrative` (arr., dép., cant., com., reg., prov.), `civil_status` (°, †, ✶, ⚭, ép., vve), `typographic_punctuation` (« », —, –, …, ’, ‘), `latin_abbr_modern` (e.g., i.e., etc., cf., ibid., op. cit., ad lib., N.B.), `bibliographic` (vol., t., p., pp., n°, fasc., éd., ms., f., r°, v°), `address` (bd, av., r., pl., imp., fbg). `get_category`, `get_expansions`, `detect_modern_markers` avec **stratégie greedy plus-long-gagne** (S.A.R. avant S.A.) et **frontières de mot adaptées** au type de marqueur (espace/ponctuation pour `M.`/`arr.`, `\b` standard pour `Mme`/`bd`, match littéral pour les Unicode `₶`/`†`/`«`). `compute_modern_archives_metrics` retourne deux scores par catégorie (pattern Sprint 56) : `strict_score` (forme abrégée préservée) et `expansion_score` (abrégée OU développée présente, casse-insensible) ; `missed_markers` distingue **pertes pures** (`expansion_preserved=False`) et **modernisations** (`expansion_preserved=True`). `modern_archives_strict_score` et `modern_archives_expansion_score` enregistrés dans le registre typé Sprint 34 pour `(TEXT, TEXT)`. **Choix éditorial assumé** : pas de classification automatique « diplomatique »/« modernisant » — c'est un outil de recherche, le chercheur lit les chiffres bruts et conclut lui-même. +75 tests (catégorisation 33 marqueurs ×9 catégories, détection par catégorie ×9, greedy plus-long-gagne, frontière de mot anti-faux-positifs, scénarios standards diplo/mod/erreur, breakdown per_category, **5 cas réalistes** clé — citation biblio, état civil, adresse, protocole royal, monnaie Ancien Régime, ponctuation typo —, dégénérés, comptage exhaustif, sanité tables, raccourcis, intégration registre). **Verrou levé** : l'extension philologique couvre désormais **trois périodes principales** des fonds patrimoniaux européens — médiéval (Sprints 56-57), imprimé ancien XVIᵉ-XVIIIᵉ (Sprint 58), archives modernes XIXᵉ-XXᵉ (ce sprint). | | 58 | **Sprint 27 du plan d'évolution 2026 — Étape 3 / extension philologique : marqueurs typographiques de l'imprimé ancien XVIᵉ-XVIIIᵉ (couche de calcul)**. Première extension du volet philologique aux périodes post-médiévales. Les Sprints 56-57 sont orientés médiéval scribal ; ce sprint cible les **éditeurs d'imprimés anciens** pour qui les marqueurs caractéristiques sont **typographiques** (composition imprimée) et non scribaux. Module `picarones/measurements/early_modern_typography.py` : 5 catégories de marqueurs (`ligatures` ff fi fl ffi ffl ſt st, `long_s` ſ, `dotless_i` ı, `ampersand` &, `nasal_tildes` ã Ã ñ Ñ õ Õ ũ Ũ ẽ Ẽ ĩ Ĩ pré-composés + séquences `voyelle + U+0303`). `get_category(char)` classe en catégorie ou None ; `detect_markers(text)` retourne `[(index, marker, category)]` reconnaissant à la fois les caractères pré-composés et les séquences combinantes ; `compute_early_modern_metrics(ref, hyp)` aligne via `difflib.SequenceMatcher` et retourne `global_preservation` + `per_category[name]={total,preserved,preservation}` + `missed_markers`. `early_modern_preservation` enregistré dans le registre typé Sprint 34 pour `(TEXT, TEXT)`. **Le breakdown par catégorie discrimine la convention typographique** : un moteur diplomatique préserve toutes les catégories ; un moteur modernisant ſ→s, fi→fi, ı→i, ã→a préserve typiquement uniquement & ; un moteur mixte panache. +38 tests dans `test_sprint58_early_modern.py` (catégorisation paramétrée 18 caractères, détection 5 catégories + tilde combinant + ordre, **trois scénarios standards** discriminés à 1.0 / 0.2 / 0.4, dégénérés, missed_markers, preserved+missed=total, sets disjoints, raccourci, intégration registre). **Verrou levé** : un benchmark sur des imprimés anciens peut désormais classer les moteurs sur leur convention typographique éditoriale — symétrique à ce que le Sprint 56 fait pour les manuscrits médiévaux. | | 57 | **Sprint 26 du plan d'évolution 2026 — Étape 3 / axe A.II.3.3 : Couverture MUFI (couche de calcul, clôture A.II.3 côté calcul)**. Suite des Sprints 55-56 dans l'axe philologique. La Medieval Unicode Font Initiative (MUFI v4.0) standardise les caractères médiévaux attendus en transcription fidèle. Module `picarones/measurements/mufi.py` : 4 plages Unicode (PUA E000-F8FF, Latin Extended-D, Combining Diacritical Marks Supplement, Alphabetic Presentation Forms) + liste explicite de lettres médiévales (þ, ð, ƿ, ſ, æ, ƀ, ȝ…), `is_mufi_char(char, custom_chars=None)` extensible, `compute_mufi_coverage` aligne caractère par caractère via difflib, retourne coverage global + per_char (total/preserved/coverage) + missed_chars. `mufi_coverage` enregistré dans le registre typé pour `(TEXT, TEXT)`. +41 tests : détection sur 28 caractères clés + plage PUA + custom_chars extensible ; coverage diplomatique → 1, modernisante → 0, partielle avec breakdown per_char ; dégénérés ; comptage exhaustif ; intégration registre. **Verrou levé** : un benchmark sur corpus médiéval peut désormais classer les moteurs sur leur couverture MUFI — critère éditorial central pour les médiévistes. **L'axe A.II.3 (philologique) est intégralement livré côté calcul.** | | 56 | **Sprint 25 du plan d'évolution 2026 — Étape 3 / axe A.II.3.2 : Score d'expansion d'abréviations médiévales (couche de calcul)**. Pour les manuscrits médiévaux, les scribes utilisent des signes d'abréviation Capelli/MUFI (ꝑ=per, ꝓ=pro, ⁊=et, p̃, q̃, etc.). Un OCR peut les **préserver** (édition diplomatique), les **développer** (édition modernisée), ou les **perdre** (erreur). Module `picarones/measurements/abbreviations.py` : table `ABBREVIATION_EXPANSIONS` (10 entrées Capelli + tilde combinant), `detect_abbreviations` (NFC/NFD-tolerant, doublons préservés), `compute_abbreviation_metrics` retourne deux scores complémentaires : `strict_score` (forme abrégée préservée) et `expansion_score` (forme abrégée OU forme développée). Frontière de mot exigée pour les expansions courtes (« et », « us »). Les deux scores enregistrés dans le registre typé Sprint 34 pour `(TEXT, TEXT)`. **Le ratio strict/expansion révèle la convention adoptée** : ≈ 1/1 → diplomatique ; 0/1 → modernisant ; 0/0 → erreur OCR. +23 tests (détection avec tilde combinant et NFD, **3 scénarios standards** discriminés, breakdown per_abbreviation, dégénérés, frontière de mot, registre, sanité table). **Verrou levé** : un benchmark sur corpus médiéval peut désormais classer les moteurs sur leur convention éditoriale (diplomatique vs modernisante) — critique pour les éditeurs de chartes. | | 55 | **Sprint 24 du plan d'évolution 2026 — Étape 3 / axe A.II.3.1 : Précision par bloc Unicode (couche de calcul, démarrage A.II.3 philologique)**. Pour un éditeur d'imprimés anciens ou un médiéviste : « quels caractères historiques ce moteur restitue-t-il fidèlement ? ». Module `picarones/measurements/unicode_blocks.py` : table de 22 blocs Unicode (Latin de Base, Latin Étendu A/B/C/D/E, Diacritiques combinants, Présentation latine, MUFI PUA…), `get_block(char)` retourne le bloc d'un caractère, `compute_unicode_block_accuracy(ref, hyp)` aligne caractère par caractère via difflib et compte les opcodes `equal` par bloc. Retourne `per_block` (correct/total/accuracy) + `global_accuracy`. Coverage exhaustive : `sum(total) == len(GT)`. `unicode_block_global_accuracy` enregistrée dans le registre typé Sprint 34 pour `(TEXT, TEXT)`. +24 tests : `get_block` sur 10 caractères clés + Other ; accuracy : identité, vide, None, substitution ciblée par bloc ; **cas réaliste du plan** : OCR modernisant ſ→s et fi→fi → 100% Latin de Base mais 0% Présentation latine ; insertions/suppressions ; coverage ; intégration registre. **Verrou levé** : un benchmark sur des imprimés anciens ou des manuscrits médiévaux peut désormais classer les moteurs sur leur fidélité aux glyphes historiques (essentiel pour les éditeurs critiques). | | 54 | **Sprint 23 du plan d'évolution 2026 — Étape 3 / axe A.II.2.2 : Layout F1 par type de région (couche de calcul, clôture A.II.2 côté calcul)**. Dernière brique de l'axe A.II.2. Pour les manuscrits glosés ou journaux multi-colonnes, répond à « le moteur sépare-t-il bien texte principal et glose ? ». Module `picarones/measurements/layout.py` : dataclass `Region(id, type, bbox)` avec validation, `_iou_bbox` (IoU de rectangles), `_align_regions` greedy par IoU décroissant avec same-type-required (pattern identique au NER Sprint 38), `compute_layout_metrics(refs, hyps, iou_threshold=0.5)` retourne global F1 + per_type + `missed_regions` (FN) + `hallucinated_regions` (FP). Type case-insensitive, coercion dict → Region, seuil ICDAR 0.5 par défaut. Pas d'enregistrement registre typé : la métrique suppose un parser ALTO/PAGE en amont (qui suivra dans un sprint dédié). +20 tests (validation Region, IoU math, cas standards : parfait, type incorrect, hallucination, FN, IoU sous/sur seuil, multi-type, greedy best-IoU wins, dégénérés, case-insensitive, shortcut). **Verrou levé** : un benchmark dont le corpus a une GT ALTO/PAGE peut désormais classer les moteurs sur leur fidélité au layout par type — métrique critique pour les médiévistes (séparation texte/glose) et les journaux multi-colonnes. | | 53 | **Sprint 22 du plan d'évolution 2026 — Étape 3 / axe A.II.2.1 : Reading order F1 ICDAR 2015 (couche de calcul)**. Métrique standard d'Antonacopoulos et al. : sur un manuscrit glosé ou un journal multi-colonnes, un moteur peut avoir un excellent CER caractère et un ordre de lecture catastrophique. Le module `picarones/measurements/reading_order.py` expose `compute_reading_order_metrics(ref_order, hyp_order)` qui calcule, pour chaque paire `(a, b)` où `a` précède `b` dans la GT, si `a` précède aussi `b` dans l'hypothèse — retourne precision/recall/F1 + détails (TP/FP/FN, régions communes vs disjointes). Conventions : doublons traités à la première occurrence, vide/None → F1 = 0 (pas de récompense gratuite), single region → 0 paire émise. Format compatible direct avec `ReadingOrderGT.region_order` du Sprint 32. `reading_order_f1` enregistré dans le registre typé Sprint 34 pour la jonction `(READING_ORDER, READING_ORDER)`. +16 tests dans `test_sprint53_reading_order.py` (canoniques : identique→1, inversé→0, permutation locale, insertion, suppression ; dégénérés : vide, single, doublons, None ; comptages ; registre). **Verrou levé** : un benchmark dont le corpus a une GT `reading_order` peut désormais classer les moteurs sur leur fidélité à l'ordre de lecture des régions ALTO/PAGE — métrique critique pour les manuscrits glosés et les journaux multi-colonnes. | | 52 | **Sprint 21 du plan d'évolution 2026 — Étape 3 / axe A.II.2.3 : différence de score Flesch (couche de calcul)**. Premier sprint de l'Étape 3 (métriques structurelles), démarré après la clôture de l'Étape 2. Nouveau module `picarones/measurements/readability.py` : `count_syllables_word` (heuristique groupes de voyelles + diacritiques FR/EN), `count_words`/`count_sentences`, `flesch_score(text, lang)` avec coefficients FR (Kandel-Moles 1958) et EN (Flesch 1948), score borné `[0, 100]`. `flesch_delta(reference, hypothesis, lang)` retourne `Flesch(OCR) - Flesch(GT)` — **positif = signal d'over-normalisation LLM**. **Aucun alignement caractère/mot requis** : la métrique reste calculable même quand l'OCR est très dégradé, ce qui en fait l'outil le plus fiable pour repérer les VLM/LLM hallucinant du texte moderne plausible mais déconnecté de la GT. `flesch_delta_fr` et `flesch_delta_en` enregistrés dans le registre typé Sprint 34 pour la jonction `(TEXT, TEXT)`. +25 tests dans `test_sprint52_readability.py` (compteurs avec cas limites, score borné, FR/EN cohérents, **cas réaliste de modernisation LLM → delta > 10 pts**, intégration registre typé). **Verrou levé** : les détecteurs narratifs futurs peuvent maintenant signaler automatiquement une over-normalisation par LLM via le delta Flesch, sans dépendre de l'alignement OCR/GT (métrique robuste aux pires cas). | | 51 | **Sprint 20 du plan d'évolution 2026 — Étape 2 / adaptation engines : Azure DI expose `Word.confidence` (clôture de l'adaptation engines)**. Suite directe des Sprints 47-50. La réponse Azure expose `analyzeResult.pages[].words[]` avec `content` et `confidence` ∈ [0, 1]. Refactor : `_run_ocr_with_result(image_path) → (text, analyze_result_dict)` centralise les deux chemins (SDK `azure-ai-documentintelligence` et REST direct via `urllib` avec polling Azure asynchrone). `_sdk_result_to_dict` convertit l'objet SDK en dict normalisé identique au REST. `_extract_token_confidences_from_result` parcourt `pages[].words[]`, filtre les confidences None/négatives et contenus vides. Texte préservé octet par octet (extraction depuis `pages[].lines[]`). Flag `expose_confidences: false`. API appelée une seule fois. +16 tests dans `test_sprint51_azure_confidences.py` (extraction multi-pages, filtrage 4 cas, cas dégénérés 4 cas, conversion SDK → dict, surcharge `run()` avec mock, échec API, intégration runner). **Verrou levé** : tous les 5 adapters OCR (Tesseract, Pero OCR, Mistral OCR, Google Vision, Azure DI) exposent désormais leurs `token_confidences` natifs — l'utilisateur obtient automatiquement ECE/MCE/reliability dans le rapport quel que soit le moteur. **L'Étape 2 du plan d'évolution 2026 est intégralement livrée bout-en-bout.** | | 50 | **Sprint 19 du plan d'évolution 2026 — Étape 2 / adaptation engines : Google Vision expose `Word.confidence`**. Suite directe des Sprints 47-49. ``DOCUMENT_TEXT_DETECTION`` expose ``Word.confidence`` au niveau mot sur ``page > block > paragraph > word``. Refactor : `_run_ocr_with_full_annotation(image_path) → (text, full_dict)` centralise les deux chemins (SDK `google-cloud-vision` et REST via `urllib`). `_sdk_full_text_to_dict` convertit le proto SDK en dict normalisé identique au REST pour traitement uniforme. `_extract_token_confidences_from_full_text` parcourt la hiérarchie et reconstruit chaque mot par concaténation des `word.symbols[i].text`. Confidence ∈ [0, 1] (format runner Sprint 42 direct). Filtrage cohérent (conf None/négative, mots vides ignorés). `TEXT_DETECTION` (mode court) → `token_confidences = None`. Flag `expose_confidences: false`. API appelée une seule fois. +17 tests dans `test_sprint50_google_vision_confidences.py` (reconstruction depuis symbols, multi-pages/blocks, filtrage 5 cas, conversion SDK → dict, surcharge `run()` avec mock, REST avec urllib mocké, intégration runner). **Verrou levé** : un benchmark Google Vision en mode `DOCUMENT_TEXT_DETECTION` produit automatiquement ECE/MCE/reliability dans le rapport. Reste Azure DI à adapter. | | 49 | **Sprint 18 du plan d'évolution 2026 — Étape 2 / adaptation engines : Mistral OCR expose ses `token_confidences` quand disponibles**. Mistral OCR a deux chemins : endpoint dédié `/v1/ocr` (qui peut exposer des `confidence` au niveau page/block/line/word selon le modèle) et API chat/vision (`pixtral-*`, sans confidences). Refactor : `_run_ocr_with_response(image_path) → (text, raw_response)` centralise les deux chemins. `_extract_token_confidences_from_response` parse la réponse en cascade — words explicites d'abord, puis propagation depuis lines/blocks (pattern Pero Sprint 48). Filtrage cohérent avec Tesseract/Pero (texte vide, conf None, conf négative ignorés). Si la réponse ne contient aucune confidence exploitable (markdown brut) ou si on est sur chat/vision, `token_confidences = None`. Flag `expose_confidences: false`. L'API est appelée une seule fois — coût identique à l'implémentation historique. +17 tests dans `test_sprint49_mistral_confidences.py` (extraction des trois niveaux, combinaison words+lines, cas dégénérés sur 5 cas, flag, surcharge `run()` avec mocks, chat/vision, échec API, intégration runner). **Verrou levé** : un benchmark Mistral OCR produit automatiquement ECE/MCE/reliability quand l'API expose ses confidences. Reste Google Vision et Azure DI à adapter. | | 48 | **Sprint 17 du plan d'évolution 2026 — Étape 2 / adaptation engines : Pero OCR expose ses `token_confidences` natifs**. Suite directe du Sprint 47 (Tesseract). Pero fournit ``line.transcription_confidence`` (probabilité CTC moyenne par ligne) ; l'adapter la propage à chaque mot de la ligne. ``PeroOCREngine.run()`` est surchargé avec un seul appel ``parser.process_page`` qui produit le ``page_layout`` ; texte ET confidences en sont extraits sans coût supplémentaire (vs Tesseract qui doit faire deux appels distincts). Refactor : ``_run_pero_pipeline(image_path) → (text, page_layout)`` centralise l'appel ; ``_run_ocr`` devient un wrapper trivial pour rétrocompat. ``_extract_token_confidences_from_layout`` parcourt regions/lines, applique ``transcription_confidence`` à chaque mot, ignore transcription vide / conf None / conf négative, retourne ``None`` si aucune ligne n'avait de confidence exploitable. Flag ``expose_confidences: false`` cohérent avec Tesseract. +14 tests dans ``test_sprint48_pero_confidences.py`` (extraction layout, multi-lignes, cas dégénérés, surcharge run avec mocks, intégration runner, fallback Pero absent). **Verrou levé** : un benchmark Pero OCR produit désormais automatiquement ECE/MCE et reliability diagram dans le rapport, sans configuration. Reste Mistral OCR, Google Vision et Azure DI à adapter. | | 47 | **Sprint 16 du plan d'évolution 2026 — Étape 2 / adaptation engines : Tesseract expose ses `token_confidences` natifs**. Premier des engines adaptés au câblage calibration du Sprint 42. `TesseractEngine.run()` est surchargé : appelle `image_to_string` pour le texte (rétrocompat octet par octet) **et** `image_to_data` pour les confidences mot par mot, retourne un `EngineResult` avec `token_confidences = [{"token": str, "confidence": float}, …]` (confidence ∈ [0, 100], le runner Sprint 42 normalise en [0, 1]). Helper `_extract_token_confidences` séparé : si `image_to_data` lève, l'OCR continue et `token_confidences = None` (warning explicite). Filtrage à la source des non-mots Tesseract (conf = -1), tokens vides, longueurs incompatibles. Nouveau paramètre config `expose_confidences: false` pour désactiver le second appel. Coût additionnel : un appel `image_to_data` par image — le texte d'`image_to_string` n'est jamais reconstruit depuis `image_to_data` (préservation stricte du comportement historique). +9 tests dans `test_sprint47_tesseract_confidences.py` (mock pytesseract, exposition, rétrocompat texte, flag `expose_confidences=False`, fallback gracieux, filtrage, intégration runner). **Verrou levé** : un benchmark Tesseract produit désormais automatiquement ECE/MCE/reliability diagram dans le rapport, sans configuration. Reste Pero, Mistral OCR, Google Vision, Azure DI à adapter. | | 46 | **Sprint 15 du plan d'évolution 2026 — Étape 2 / axe A.III : vue HTML stratifiée + détecteur narratif (clôture A.III)**. Suite directe du Sprint 45 (couche backend). Nouveau module `picarones/report/stratification_render.py` : `build_stratified_ranking_html` rend un `
` natif (collapsible sans JS) par strate avec tableau moteur × (médiane, moyenne, docs), cellule médiane colorée par gradient vert→rouge, premier `
` ouvert par défaut, bandeau d'avertissement en tête si `corpus_homogeneity` fourni. `_build_report_data` expose `available_strata`/`stratified_ranking`/`corpus_homogeneity` au top-level ; `view_ranking.html` insère le bloc après le tableau principal **uniquement si stratification disponible**. Nouveau `FactType.STRATIFICATION_RECOMMENDED` (priority 45, importance MEDIUM ou HIGH selon le gap) + détecteur `detect_stratification_recommended` (seuil 5 points / 10 points de CER inter-strate). Templates FR/EN sans nombres en dur. L'arbitre marque la paire `{GLOBAL_LEADER_CER, STRATIFICATION_RECOMMENDED}` comme complémentaire. +8 clés i18n FR/EN. Anti-injection HTML via `html.escape`. +38 tests dans `test_sprint46_stratification_html.py`. **Verrou levé** : A.III (stratification) est désormais livré bout-en-bout — couche backend (Sprint 45) + vue HTML + détecteur narratif (Sprint 46) ; le lecteur du rapport voit immédiatement quand le corpus est hétérogène et est invité à consulter la vue stratifiée. | | 45 | **Sprint 14 du plan d'évolution 2026 — Étape 2 / axe A.III : stratification par `script_type` (couche backend)**. Première brique de la « plus haute valeur ajoutée transversale » du plan. `BenchmarkResult.doc_strata: Optional[dict[str, str]]` ajouté (map `{doc_id: script_type}` capturée par le runner avant `compact()` qui efface `image_quality`). Trois nouvelles méthodes : `available_strata()` (liste triée des strates distinctes, ignore les vides) ; `stratified_ranking()` qui retourne `{stratum: [ranking_entry]}` avec mean/median CER recalculés par strate, tri par médiane (Sprint 44), inclut les moteurs absents d'une strate sous forme d'entrée dégénérée (mean/median = None) ; `corpus_homogeneity()` qui pour le moteur leader global retourne l'écart inter-strate de la médiane CER et la paire min/max — base du futur avertissement « ce corpus est hétérogène ». `as_dict()` expose les nouveaux champs quand renseignés (rétrocompat stricte sinon). +16 tests dans `test_sprint45_stratification.py` couvrant champ, available_strata, stratified_ranking (1 entrée/moteur/strate, métriques per-strate, tri par médiane, moteurs absents), corpus_homogeneity, sérialisation, et un **test propriété réaliste** : le leader global peut perdre sur une strate (Tesseract domine globalement mais Pero gagne sur le manuscrit). **Verrou levé** : la couche d'agrégation par strate est en place ; la vue HTML stratifiée + toggle UI viendront dans un sprint dédié, et un détecteur narratif `STRATIFICATION_RECOMMENDED` peut maintenant lire `corpus_homogeneity()` pour suggérer la vue stratifiée. | | 44 | **Sprint 13 du plan d'évolution 2026 — Étape 2 / axe A.I.2 : tri par médiane par défaut + détecteur d'asymétrie**. Réponse à la critique structurelle 2 du plan : sur les corpus patrimoniaux, la moyenne est tirée par quelques documents catastrophiques et masque les performances réelles. `EngineReport.median_cer` ajouté (lit `aggregated_metrics["cer"]["median"]`). `BenchmarkResult.ranking()` inclut désormais `median_cer` dans chaque entrée et **trie par médiane CER croissante par défaut** (fallback sur `mean_cer` si médiane absente). Nouveau `FactType.MEDIAN_MEAN_GAP_WARNING` + détecteur `detect_median_mean_gap_warning` (priority 140) : émet un Fact quand `\|mean - median\| / median > 30 %` pour le moteur leader, importance HIGH si gap relatif ≥ 100 % (sinon MEDIUM). Garde-fou : ne déclenche pas si médiane nulle. Templates FR/EN sans nombres en dur (vérifié). L'arbitre marque la paire `{GLOBAL_LEADER_CER, MEDIAN_MEAN_GAP_WARNING}` comme **complémentaire** : les deux phrases peuvent coexister dans la synthèse pour nuancer le leader. +15 tests dans `test_sprint44_median_default.py` (propriété, tri sur cas asymétrique réaliste, fallback, déclenchement détecteur sur 4 cas dégénérés, importance, traçabilité anti-hallucination FR + EN, intégration build_synthesis). **Verrou levé** : la critique « le rapport classe sur la moyenne alors que les distributions patrimoniales sont asymétriques » est résolue ; le lecteur voit immédiatement le moteur le plus représentatif et est averti quand l'écart médiane/moyenne est suspect. | | 43 | **Sprint 12 du plan d'évolution 2026 — Étape 2 / axe A.II.1.b : vue HTML calibration (clôture A.II.1.b côté rapport)**. Nouveau module `picarones/report/calibration_render.py` : `build_calibration_summary_html` rend un tableau résumé (ECE, MCE, accuracy moyenne, confidence moyenne, n_predictions, doc_count) avec cellule ECE colorée par gradient vert (bien calibré) → rouge (mal calibré) ; `build_reliability_diagram_svg` rend un SVG par moteur avec barres d'accuracy par bin, ligne reliant les points `(avg_confidence, accuracy)`, diagonale en pointillé pour la calibration parfaite, axes annotés (graduations 0/0.5/1) ; `build_reliability_diagrams_grid_html` génère une grille auto-fit (un SVG par moteur ayant `aggregated_calibration`). Rendu strictement server-side, pas de JS, déterministe. `_build_report_data` expose `aggregated_calibration` par moteur ; `ReportGenerator.generate` calcule les blocs et les passe à `view_analyses.html` qui les affiche **uniquement si ≥ 1 moteur a un `aggregated_calibration`** (rapport adaptatif). Anti-injection HTML via `html.escape`. +13 clés i18n FR/EN. +43 tests dans `test_sprint43_calibration_html.py` couvrant le rendu (résumé, SVG, grille), le masquage adaptatif, l'anti-injection, l'intégration FR + EN, la complétude i18n. **Verrou levé** : A.II.1.b (calibration) est désormais visible bout-en-bout dans le rapport — il manque uniquement l'adaptation effective des engines pour exposer leurs confidences natives (un sprint par adapter : Tesseract `image_to_data`, Pero `PageLayout`, Mistral `confidence`, Google Vision `Word.confidence`, Azure DI). | | 42 | **Sprint 11 du plan d'évolution 2026 — Étape 2 / axe A.II.1.b : exposition `token_confidences` + câblage runner**. Suite du Sprint 39 (couche de calcul). `EngineResult` gagne un champ optionnel `token_confidences: Optional[list[dict[str, Any]]]` (`None` par défaut → rétrocompat stricte). `DocumentResult.calibration_metrics` et `EngineReport.aggregated_calibration` ajoutés (sérialisation dans `as_dict` conditionnelle, libérés par `compact()`). Nouveau helper `_calibration_from_engine_result` qui aligne par bag-of-words avec multiplicité (proxy oracle, comme `oracle_token_recall`), normalise les confidences en pourcentage à `[0, 1]`, ignore les confidences négatives (Tesseract met -1 pour les non-mots) ; appelé dans `_compute_document_result` quand `token_confidences` est non-vide. Helper `_aggregate_calibration` combine les bins de tous les docs en somme pondérée par count, recalcule ECE/MCE micro. **L'adaptation de chaque adapter (Tesseract, Pero OCR, Mistral OCR, Google Vision, Azure DI) à exposer ses confidences natives est reportée à des sprints dédiés** : ce sprint pose l'infrastructure complète et la teste avec un mock. +17 tests dans `test_sprint42_calibration_runner.py` (champ EngineResult, sérialisation/compact, helper d'alignement avec calibration parfaite + normalisation % + skip négatifs + bag-of-words multiplicité, agrégation multi-docs, rétrocompat sans confidences). **Verrou levé** : un moteur qui expose ses confidences (cas réel à venir) verra automatiquement ses métriques de calibration calculées et agrégées par le runner — il manque uniquement la vue HTML reliability et l'adaptation des engines un par un. | | 41 | **Sprint 10 du plan d'évolution 2026 — Étape 2 / axe A.II.1.a : vue HTML NER (clôture A.II.1.a)**. Nouveau module `picarones/report/ner_render.py` : `build_ner_summary_html` rend un tableau résumé (F1 global, P, R, docs évalués, hallucinations, missed) avec cellule F1 colorée par gradient rouge → jaune → vert ; `build_ner_per_category_html` rend la heatmap moteur × catégorie d'entité (PER, LOC, ORG, DATE, MISC…) avec tooltip `support=N`, cellule vide marquée `—` pour les catégories non observées. Rendu server-side, pas de JS, déterministe. Anti-injection HTML via `html.escape`. `_build_report_data` expose `aggregated_ner` par moteur. `ReportGenerator.generate` calcule les deux blocs et les passe au template `view_analyses.html` qui les affiche dans une `chart-card` à largeur pleine **uniquement si ≥ 1 moteur a un `aggregated_ner`**. +12 clés i18n FR/EN. +38 tests dans `test_sprint41_ner_html.py` (rendu, masquage adaptatif, anti-injection, intégration FR + EN, complétude i18n). **Verrou levé** : A.II.1.a (NER) est désormais livré bout-en-bout — couche de calcul (Sprint 38) + backend + câblage runner (Sprint 40) + vue HTML (Sprint 41). Reste la calibration A.II.1.b à finir bout-en-bout (extraction des token_confidences depuis les engines + vue HTML reliability diagram). | | 40 | **Sprint 9 du plan d'évolution 2026 — Étape 2 / axe A.II.1.a : NER backend + câblage runner**. Suite du Sprint 38 (couche de calcul). Nouveau module `picarones/measurements/ner_backends.py` : `EntityExtractor` (Protocol, tout callable `(text) → list[dict]` est valide), `SpacyEntityExtractor` (lazy-import spaCy, charge le modèle au premier appel, fallback gracieux silencieux + warning explicite si spaCy/modèle absent, mapping par défaut spaCy → conventions HIPE : PERSON→PER, GPE→LOC, etc.), `SPACY_PROFILES` (6 profils nommés), `get_extractor(profile)`, `is_spacy_available()`. `DocumentResult.ner_metrics: Optional[dict]` et `EngineReport.aggregated_ner` ajoutés (sérialisés dans `as_dict` quand renseignés, libérés par `compact()`). `runner.run_benchmark` accepte un nouveau paramètre optionnel `entity_extractor` ; si fourni, helpers `_attach_ner_metrics` et `_aggregate_ner` calculent les métriques en post-process (main process pour éviter de pickler spaCy dans les sous-processus). Rétrocompat stricte : sans `entity_extractor`, aucun calcul ni champ ajouté. Nouveau extra `[ner]` dans `pyproject.toml` (spacy>=3.7.0). +16 tests dans `test_sprint40_ner_runner.py` (fallback sans spaCy + warning, idempotence load, profils + factory, sérialisation nouveaux champs, câblage runner avec mock injecté, agrégation micro-F1, rétrocompat sans extracteur, robustesse à un extracteur qui lève). **Verrou levé** : un benchmark dont le corpus a une GT entités produit maintenant des métriques NER bout-en-bout — il manque uniquement la vue HTML dédiée (Sprint 41 à venir). | | 39 | **Sprint 8 du plan d'évolution 2026 — Étape 2 / axe A.II.1.b : Calibration (couche de calcul)**. Nouveau module `picarones/measurements/calibration.py` avec dataclass `CalibrationBin` (`bin_low/high`, `avg_confidence`, `accuracy`, `count`, propriété `gap`), `reliability_diagram`, `expected_calibration_error` (ECE — moyenne pondérée par bin de `\|conf - accuracy\|`, ∈ [0, 1]), `maximum_calibration_error` (MCE — pire écart sur les bins non vides), `compute_calibration_metrics` (vue agrégée). Calcul d'index de bin par multiplication `int(c * n_bins)` plutôt que division pour éviter le piège IEEE 754 (`0.6 / 0.1 = 5.999…`). Aucune dépendance externe — les listes `confidences` ∈ [0, 1] et `is_correct` ∈ {0,1} sont fournies en entrée ; l'extraction depuis les engines existants est reportée à un sprint dédié. +32 tests couvrant calibration parfaite (ECE = 0), cas extrêmes (sur/sous-confiance → ECE = 0,5), biais constant (ECE = `\|c-a\|`), binning correct (0.6 placé dans le bon bin), bins vides (`gap = None`), garde-fous, monotonie `n_bins` plus fins → ECE ne décroît pas. **Verrou levé** : un workflow patrimonial peut maintenant répondre à *« quand le moteur dit qu'il est sûr, est-il vraiment sûr ? »* — différence entre vérification humaine systématique (100 %) et ciblée (15 %) sur les passages à faible confiance. | | 38 | **Sprint 7 du plan d'évolution 2026 — Étape 2 / axe A.II.1.a : NER (couche de calcul)**. Nouveau module `picarones/measurements/ner.py` : dataclass `Entity(label, start, end, text)` (validation de span), fonction `compute_ner_metrics(reference, hypothesis, iou_threshold=0.5)` qui aligne par chevauchement IoU (greedy, IoU décroissant, chaque entité matchée au plus une fois) et retourne precision/recall/F1 globaux + par catégorie + listes `hallucinated_entities` / `missed_entities`. Format dict compatible `EntitiesGT` du Sprint 32. Métrique `ner_f1` enregistrée dans le registre typé Sprint 34 pour la jonction `(ENTITIES, ENTITIES)`. Aucune dépendance externe : les listes d'entités sont fournies en entrée — le backend extracteur (spaCy/Stanza/HIPE) suivra dans un sprint dédié. +19 tests dans `test_sprint38_ner_metrics.py` (cas standards, label case-insensitive, IoU sous/sur seuil, multi-catégorie, alignement greedy, cas dégénérés, validation Entity, intégration registre). **Verrou levé** : un benchmark dont le corpus a une GT entités peut maintenant mesurer l'utilité aval pour l'indexation prosopographique — métrique critique pour les bibliothèques numériques. | | 37 | **Sprint 6 du plan d'évolution 2026 — Étape 2 / axe A : section inter-moteurs dans le rapport HTML**. Nouveau module `picarones/report/inter_engine_render.py` qui produit deux blocs HTML serveur-side (pas de JS) : `build_divergence_matrix_html` rend une table heatmap CSS inline (gradient blanc → rouge sur le max hors-diagonale, diagonale étiquetée, paire la plus divergente annoncée en sous-titre) ; `build_oracle_gap_html` rend l'encart factuel best engine / recall / oracle / gap absolu+relatif / doc count. Le `ReportGenerator` les calcule et les passe au template `view_analyses.html` qui les affiche dans une `chart-card` à largeur pleine **uniquement si présents** — principe du rapport adaptatif (< 2 moteurs ou pas de taxonomie → section omise). +14 clés i18n FR/EN (`h_inter_engine`, `inter_engine_note`, `divergence_*`, `oracle_*`). Anti-injection HTML via `html.escape`. +42 tests dans `test_sprint37_inter_engine_html.py` couvrant le rendu (valeurs, paire max), le masquage adaptatif sur 4 cas dégénérés, l'anti-injection (engine name `