Spaces:
Running
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.
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
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-multipartdes dépendances : FastAPI vérifie sa présence à l'import du module (décoration@app.postavecUploadFile), pas à l'exécution. Ça casse tous les tests web au setup. - Ne jamais mettre
except Exception: pass: remplacer parlogger.warning("[module] fonctionnalité dégradée : %s", e). - Toujours utiliser
logger.warningavec message explicite quand une fonctionnalité optionnelle échoue (confusion, taxonomy, structure, image_quality, etc.). - Avant tout push, lancer
make lint(ouruff check picarones/ tests/). La config est centralisée danspyproject.tomlsous[tool.ruff], donc CI, Makefile et invocation directe produisent le même résultat. Le joblintdu 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/profilesdoit les lire dynamiquement depuis ce fichier, pas depuis une liste statique.
Variables d'environnement
# 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 secretHF_TOKENdans 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 ` |
| 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( |
| 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 <Q1 plus facile, rouge 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 |
| 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 <spec.yaml> --corpus <dir> (avec --output-json/--output-html/--lang) et compare <specs.yaml> --corpus <dir> (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 <type>.<metric>: mean (n=N), error_breakdown catégorisé), build_pipeline_report_html(bench, lang) (document HTML autonome <!doctype html> + 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 : <type>@<step> (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 <details> natif (collapsible sans JS) par strate avec tableau moteur × (médiane, moyenne, docs), cellule médiane colorée par gradient vert→rouge, premier <details> 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 <script> correctement échappé), l'intégration rapport FR + EN, la complétude i18n sur les 14 clés × 2 langues. Verrou levé : ce que le moteur narratif annonce dans la synthèse (« tess et pero ont des profils divergents… ») est maintenant aussi visible dans la vue analyses sous forme de matrice et d'encart factuel — le lecteur peut vérifier visuellement le chiffre. |
| 36 | Sprint 5 du plan d'évolution 2026 — Étape 2 / axe A : câblage inter-moteurs au runner et au moteur narratif. Suite du Sprint 35 : inter_engine.py gagne compute_inter_engine_analysis (agrégation corpus-wide doc par doc, structure stable consommable par les détecteurs et le rapport HTML — oracle global, recall par moteur, per_doc top 50 trié par gap, matrice de divergence, paire la plus divergente). BenchmarkResult expose un nouveau champ optionnel inter_engine_analysis ; le runner (run_benchmark) collecte les hypothèses brutes par moteur avant compact() et calcule l'analyse si ≥ 2 moteurs (sinon None). Nouveau FactType.ENSEMBLE_OPPORTUNITY (priority 130, importance MEDIUM, HIGH si relative_gap ≥ 50 %) avec détecteur detect_ensemble_opportunity qui fallback sur per_engine_recall quand la divergence taxonomique est absente. Templates FR/EN ajoutés à narrative/templates/{fr,en}.yaml. report_data["inter_engine_analysis"] exposé pour la consommation par le rapport HTML (matrice de divergence Sprint 37 à venir). +22 tests dans test_sprint36_ensemble_narrative.py couvrant l'agrégation, l'exposition BenchmarkResult.as_dict, les seuils du détecteur, le fallback paire sans taxonomie, l'intégration build_synthesis FR + EN, la traçabilité anti-hallucination (chaque nombre rendu est dans le payload, template sans chiffres en dur). |
Moteur narratif
Le modèle de données (Fact, FactType, FactImportance,
DetectorRegistry) vit en cercle 1 dans
picarones/core/facts.py. Les détecteurs et
le rendu vivent en cercle 2 :
picarones/measurements/narrative/
├── __init__.py API publique + pipeline build_synthesis
├── arbiter.py Tri par importance, non-redondance, anti-contradiction
├── renderer.py Rendu templates YAML par str.format_map (déterministe)
├── registry.py Registre par défaut des détecteurs
├── templates/{fr,en}.yaml 18 templates × 2 langues
└── detectors/ 18 détecteurs en 6 familles
├── ranking.py 5 (global_leader, statistical_tie, significant_gap,
│ speed_winner, median_mean_gap_warning)
├── pareto.py 2 (pareto_alternative, cost_outlier)
├── stratum.py 3 (stratum_winner, stratum_collapse,
│ stratification_recommended)
├── quality.py 4 (error_profile_outlier, llm_hallucination_flag,
│ robustness_fragile, confidence_warning)
├── history.py 3 (engine_off_baseline, engine_unstable,
│ regression_in_history)
└── ensemble.py 1 (ensemble_opportunity)
Principe anti-hallucination : chaque valeur numérique ou nom d'entité
dans le payload d'un Fact provient du JSON d'entrée. Le test
test_sprint19_narrative_engine.py parse la synthèse rendue et vérifie
la traçabilité.
Règle anti-contradiction (arbitre) : si SIGNIFICANT_GAP (Wilcoxon
non corrigé) et STATISTICAL_TIE (Nemenyi corrigé) concernent les mêmes
moteurs, Nemenyi l'emporte.
Pipeline : build_synthesis(benchmark_data, lang, max_facts=5)
détecte, arbitre, rend.
Contexte développement
- Environnement : GitHub Codespaces, Python 3.11+
- Tests :
pytest tests/ -q→ ~3354 passed, 2 skipped, 0 failed. - Plan d'évolution actif :
docs/roadmap/evolution-2026.md. - Manifeste architecture :
docs/architecture.md. - API publique stable :
docs/api-stable.md. - Branche active :
claude/code-quality-audit-ACnhK.