# Changelog — Picarones Tous les changements notables de ce projet sont documentés dans ce fichier. Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/). La numérotation de version suit [Semantic Versioning](https://semver.org/lang/fr/). --- ## [1.2.x] — Sprints 32+ — 2026-04 → ongoing > Démarrage de la **Phase 0** du [plan d'évolution 2026](docs/roadmap/evolution-2026.md) : > fondations communes pour l'enrichissement métrique (axe A) et le banc > d'essai de pipelines composées (axe B). Les deux axes restent > rétrocompatibles avec le mode benchmark texte historique. ### Ajouté - **Sprint 53 — A.II.2.1 Reading order F1 (ICDAR 2015) : couche de calcul.** Suite du Sprint 52 dans l'axe A.II.2 (métriques structurelles). 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 CER seul ne capture pas cette dimension. - Nouveau module `picarones/core/reading_order.py` : - ``compute_reading_order_metrics(ref_order, hyp_order)`` : pour chaque paire ``(a, b)`` où ``a`` précède ``b`` dans la GT, vérifie si ``a`` précède aussi ``b`` dans l'hypothèse. Retourne precision/recall/F1 + détails (TP/FP/FN, paires totales, régions communes vs disjointes). - ``reading_order_f1`` : raccourci qui retourne juste le F1. - Conventions : doublons traités à la première occurrence, séquences ``None``/vides → F1 = 0 (pas de récompense gratuite), séquence à 1 région → 0 paire émise → F1 = 0 (convention de bord). - Format compatible avec ``ReadingOrderGT.region_order`` du Sprint 32 — l'utilisateur fournit directement la liste d'IDs. - ``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` (cas canoniques : identique → F1=1, inversé → F1=0, permutation locale, insertion, suppression ; cas dégénérés : vide, single region, doublons, None ; comptages détaillés ; intégration registre typé). - **Sprint 52 — A.II.2.3 Différence Flesch : couche de calcul (démarrage de l'Étape 3 / axe A — métriques structurelles).** Stratégie identique aux Sprints 35/38/39 (couche pure d'abord, câblage runner+HTML après). - Nouveau module `picarones/core/readability.py` : - ``count_syllables_word`` : heuristique groupes de voyelles consécutives (avec diacritiques FR/EN), fallback à 1 syllabe pour les mots sans voyelle (acronymes type « BNF »). - ``count_words`` (regex Unicode) et ``count_sentences`` (découpe sur ``.!?…``, minimum 1 si le texte contient au moins un mot). - ``flesch_score(text, lang)`` avec coefficients FR (Kandel-Moles 1958, ``207 - 1.015·m/p - 73.6·s/m``) et EN (Flesch 1948, ``206.835 - 1.015·m/p - 84.6·s/m``). Score borné dans ``[0, 100]``. - ``flesch_delta(reference, hypothesis, lang)`` retourne la différence ``Flesch(OCR) - Flesch(GT)``. **Positif = signal d'over-normalisation LLM** (le LLM a lissé la langue historique). - **Aucun alignement caractère/mot requis** : la métrique reste calculable même quand l'OCR est très dégradé — c'est l'avantage clé pour repérer les VLM/LLM qui hallucinent 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 de base avec cas limites, score Flesch borné, FR/EN cohérents, delta nul sur textes identiques, **cas réaliste de modernisation LLM** → delta > 10 pts, OCR dégradé borné, intégration registre typé). - **Sprint 51 — Adapter Azure Document Intelligence : exposition de `Word.confidence` (clôture de l'adaptation engines).** Suite directe des Sprints 47-50. Azure DI expose ``analyzeResult.pages[].words[]`` avec ``content`` et ``confidence`` ∈ [0, 1]. L'adapter parcourt cette hiérarchie et émet une entrée par mot au format Sprint 42. - 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 à la réponse REST pour traitement uniforme. - ``_extract_token_confidences_from_result`` parcourt ``pages[].words[]``, extrait ``content`` et ``confidence``, filtre les confidences None / négatives et les contenus vides. - Le texte ``EngineResult.text`` est extrait depuis ``pages[].lines[]`` (préservation rétrocompat octet par octet). - Flag config ``expose_confidences: false``. - L'API est appelée une seule fois — aucun overhead. - +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). - **Sprint 50 — Adapter Google Vision : exposition de `Word.confidence`.** Suite directe des Sprints 47-49. ``DOCUMENT_TEXT_DETECTION`` expose ``Word.confidence`` au niveau mot sur ``page > block > paragraph > word`` ; l'adapter parcourt cette hiérarchie et émet une entrée par mot au format Sprint 42. - Refactor : ``_run_ocr_with_full_annotation(image_path) → (text, full_dict)`` centralise les deux chemins (SDK ``google-cloud-vision`` et REST direct via ``urllib``). ``_run_ocr`` reste rétrocompat (retourne juste le texte). - ``_sdk_full_text_to_dict`` convertit la réponse proto SDK en dict normalisé identique à la réponse REST, pour traitement uniforme. - ``_extract_token_confidences_from_full_text`` parcourt ``pages → blocks → paragraphs → words``, reconstruit chaque mot par concaténation des ``word.symbols[i].text``, et émet ``{"token": str, "confidence": float}`` (confidence ∈ [0, 1] — le runner Sprint 42 accepte directement ce format). - Filtrage cohérent avec les autres adapters : confidence None / négative → ignorée, mots vides → ignorés. - ``TEXT_DETECTION`` (mode "court") ne fournit pas de confidence par mot → ``token_confidences = None``. - Flag config ``expose_confidences: false``. - L'API est appelée une seule fois — aucun overhead. - +17 tests dans `test_sprint50_google_vision_confidences.py` (reconstruction depuis symbols, multi-pages/blocks, filtrage, flag, cas dégénérés, conversion SDK → dict, surcharge ``run()`` avec mock du chemin réseau, REST avec urllib mocké, intégration runner). - **Sprint 49 — Adapter Mistral OCR : exposition des `token_confidences` quand l'API les fournit.** Suite des Sprints 47 (Tesseract) et 48 (Pero OCR). Mistral OCR a deux chemins : l'endpoint dédié `/v1/ocr` (modèle `mistral-ocr-latest`) qui peut exposer des champs `confidence` à différents niveaux, et l'API chat/vision (`pixtral-*`) qui ne fournit pas de confidences. - Refactor : nouvelle méthode `_run_ocr_with_response(image_path)` retourne `(text, raw_response)`. `_run_ocr_native_api` retourne désormais aussi le JSON brut. Le chemin chat/vision retourne `(text, None)` car aucune confidence n'est disponible. - `_extract_token_confidences_from_response` parse la réponse `/v1/ocr` en cascade : 1. `pages[i].words[j]` avec `{"text", "confidence"}` → extraction directe 2. `pages[i].lines[j]` avec `{"text", "confidence"}` → propagation de la confidence à chaque mot (pattern Pero Sprint 48) 3. `pages[i].blocks[j]` → idem - Filtrage cohérent avec Tesseract/Pero : texte vide, confidence None, confidence négative → ignorés. - Si l'API ne retourne aucun champ `confidence` exploitable (cas courant si Mistral retourne uniquement du markdown), ou si on est sur le chemin chat/vision, `token_confidences = None`. - Nouveau paramètre config `expose_confidences: false` cohérent avec les autres adapters. - L'API est appelée **une seule fois** ; le coût est strictement identique à l'implémentation historique. - +17 tests dans `test_sprint49_mistral_confidences.py` couvrant l'extraction (words explicites, propagation lines/blocks, combinaison, filtrage texte vide / conf None / négative), les cas dégénérés (None, dict vide, pas de pages, markdown sans confidences, types invalides), le flag `expose_confidences=False`, la surcharge `run()` (mock du chemin réseau, chat/vision sans confidences, échec API), et l'intégration runner. - **Sprint 48 — Adapter Pero OCR : exposition des `token_confidences` natifs.** Suite directe du Sprint 47 (Tesseract). Pero OCR fournit une confidence par ligne (``transcription_confidence``, probabilité CTC moyenne) ; l'adapter la propage à chaque mot de la ligne. - ``PeroOCREngine.run()`` surchargé : un seul appel ``parser.process_page`` 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 au pipeline ; ``_run_ocr`` devient un wrapper trivial pour rétrocompat. - ``_extract_token_confidences_from_layout`` parcourt regions/lines, applique ``transcription_confidence`` à chaque mot de la ligne, ignore les transcriptions vides / confidences None / confidences négatives, retourne ``None`` si aucune ligne n'avait de confidence exploitable. - Nouveau paramètre config ``expose_confidences: false`` (cohérent avec Tesseract Sprint 47). - Pipeline appelé une seule fois → **aucun overhead** par rapport à l'implémentation historique (vs un appel supplémentaire pour Tesseract). - +14 tests dans ``test_sprint48_pero_confidences.py`` couvrant : extraction depuis layout (tokens uniques, multi-lignes, transcription vide, confidence None / négative), flag ``expose_confidences=False``, cas dégénérés (None / regions vides / aucune confidence), surcharge ``run()`` (texte préservé octet par octet, échec du pipeline), intégration runner avec ``calibration_metrics`` correctement calculée, fallback gracieux quand pero-ocr est absent. - **Sprint 47 — Adapter Tesseract : exposition des `token_confidences` natifs.** Premier des engines adaptés au câblage calibration (Sprint 42). L'utilisateur qui benchmarke avec Tesseract obtient désormais automatiquement ECE/MCE et reliability diagram dans le rapport, sans configuration supplémentaire. - `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é du chemin OCR principal : si `image_to_data` lève, l'OCR continue normalement et `token_confidences = None` (warning explicite, pas `except: pass`). - Filtrage à la source : non-mots Tesseract (conf = -1), tokens vides, longueurs incompatibles → ignorés. - Nouveau paramètre config `expose_confidences: false` pour désactiver le second appel Tesseract (économie d'un appel par image en cas de besoin). - Coût additionnel : un appel `image_to_data` par image. Le texte de `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` couvrant l'exposition des confidences (avec mock pytesseract), la préservation octet par octet du texte, le flag `expose_confidences=False`, le fallback gracieux quand `image_to_data` lève (warning + `None`), le filtrage des non-mots/longueurs incompatibles, l'intégration bout-en-bout avec le runner (`calibration_metrics` calculé), et le cas pytesseract absent. - **Sprint 46 — A.III stratification par `script_type` : vue HTML + détecteur narratif (clôture A.III)**. Suite directe du Sprint 45 (couche backend). La vue stratifiée est désormais rendue dans le rapport et un détecteur signale automatiquement les corpus hétérogènes. - Nouveau module `picarones/report/stratification_render.py` : `build_stratified_ranking_html` rend un `
` natif (collapsible sans JS) par strate avec tableau moteur × (médiane, moyenne, docs). Cellule médiane colorée par gradient vert (faible CER) → rouge (élevé). Premier `
` ouvert par défaut pour donner le contexte. Bandeau d'avertissement en tête si `corpus_homogeneity` fourni (écart inter-strate du leader). - `_build_report_data` expose `available_strata`, `stratified_ranking`, `corpus_homogeneity` au top-level. Le bloc HTML est passé au template `view_ranking.html` qui l'insère après le tableau principal **uniquement si stratification disponible** (rapport adaptatif). - Nouveau `FactType.STRATIFICATION_RECOMMENDED` (priority 45, importance MEDIUM ou HIGH selon le gap) avec détecteur `detect_stratification_recommended` qui lit `corpus_homogeneity` et émet un Fact quand le gap inter-strate du leader dépasse 5 points de CER (HIGH au-delà de 10 points). Templates FR/EN sans nombres en dur. - L'arbitre marque la paire `{GLOBAL_LEADER_CER, STRATIFICATION_RECOMMENDED}` comme **complémentaire** : la recommandation peut cohabiter avec la phrase du leader pour nuancer. - +8 clés i18n FR/EN pour la vue stratifiée (`stratification_caption`, `stratification_description`, `stratification_*_label`, `stratification_gap_summary`). - Anti-injection HTML via `html.escape` sur les noms de moteurs et les noms de strates. - +38 tests dans `test_sprint46_stratification_html.py` couvrant le rendu (un `
` par strate, métriques visibles, premier ouvert), le bandeau d'hétérogénéité, le masquage adaptatif (4 cas), l'anti-injection (engine et stratum avec balises HTML), les seuils du détecteur (4 cas), la traçabilité anti-hallucination FR + EN, l'absence de chiffres en dur dans les templates, l'intégration `ReportGenerator` FR + EN, et la complétude i18n. - **Sprint 45 — A.III stratification par `script_type` : couche d'agrégation backend.** Première brique de la « plus haute valeur ajoutée transversale » du plan d'évolution. Le rapport peut désormais classer les moteurs **par strate** (manuscrit gothique, cursive administrative, imprimé ancien, humanistique…) — la moyenne globale ment quand le corpus est hétérogène. - `BenchmarkResult.doc_strata: Optional[dict[str, str]]` : map ``{doc_id: script_type}`` capturée par le runner avant ``compact()`` (qui efface ``image_quality``). - `BenchmarkResult.available_strata()` : liste triée des strates distinctes, ignore les valeurs vides. - `BenchmarkResult.stratified_ranking()` : retourne ``{stratum: [ranking_entry]}`` avec mean/median CER **recalculés par strate**, tri par médiane (cohérent avec Sprint 44). Inclut les moteurs sans aucun doc dans une strate sous forme d'entrée dégénérée (mean/median = None). - `BenchmarkResult.corpus_homogeneity()` : pour le moteur leader global, retourne l'écart inter-strate de la médiane CER et identifie la paire de strates min/max — base du futur avertissement automatique « ce corpus est hétérogène, consultez la vue stratifiée ». - `as_dict()` expose `doc_strata`, `available_strata`, `stratified_ranking`, `corpus_homogeneity` quand renseignés (rétrocompat stricte sinon — clés absentes). - Le runner peuple `doc_strata` avant compact en lisant ``DocumentResult.image_quality["script_type"]``. - +16 tests dans `test_sprint45_stratification.py` couvrant les fields, available_strata, stratified_ranking (1 entrée/moteur/ strate, métriques per-strate, tri par médiane, moteurs absents), corpus_homogeneity (None < 2 strates, calcul d'écart), sérialisation as_dict, et un **test propriété réaliste** : le leader global peut perdre sur une strate (Tesseract domine globalement mais perd sur le manuscrit où Pero gagne). - **Sprint 44 — A.I.2 : tri par médiane CER par défaut + détecteur d'asymétrie.** Réponse à la critique structurelle 2 du plan d'évolution : sur les corpus patrimoniaux, la moyenne est facilement tirée par quelques documents catastrophiques et masque les performances réelles ; la médiane est plus représentative. - `EngineReport.median_cer` : nouvelle propriété qui lit `aggregated_metrics["cer"]["median"]`. - `BenchmarkResult.ranking()` : - inclut désormais `median_cer` dans chaque entrée (additif) - **trie par médiane CER croissante par défaut** (et non plus par moyenne) - retombe sur `mean_cer` quand `median_cer` est absent (rétrocompat pour le cas pathologique) - Nouveau `FactType.MEDIAN_MEAN_GAP_WARNING` et détecteur `detect_median_mean_gap_warning` (priority 140) : émet un Fact quand `|mean - median| / median > 30 %` pour le moteur leader. Importance MEDIUM par défaut, HIGH si gap relatif ≥ 100 %. Garde-fou : ne déclenche pas si la médiane est nulle. - Templates FR/EN — aucun nombre en dur, tout vient du payload (vérifié par test). - 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é median_cer, tri par médiane sur cas asymétrique réaliste, fallback sur la moyenne, déclenchement du détecteur sur 4 cas dégénérés, importance MEDIUM/HIGH selon gap, traçabilité anti-hallucination FR + EN, intégration via build_synthesis). - **Sprint 43 — A.II.1.b Calibration : vue HTML reliability diagram + tableau ECE/MCE (clôture A.II.1.b côté rapport).** Suite directe du Sprint 42 (câblage runner). Les chiffres de calibration sont désormais visibles dans le rapport HTML. - Nouveau module `picarones/report/calibration_render.py` : - `build_calibration_summary_html(engines_summary, labels)` : tableau résumé par moteur (ECE, MCE, Précision moyenne, Confiance moyenne, n_predictions, doc_count). Cellule ECE colorée par gradient vert (bien calibré) → rouge (mal calibré). - `build_reliability_diagram_svg(aggregated_calibration, labels, engine_name)` : SVG d'un reliability diagram avec barres d'accuracy par bin, ligne reliant les points `(avg_confidence, accuracy)`, diagonale de référence en pointillé, axes annotés (graduations 0/0.5/1). - `build_reliability_diagrams_grid_html(engines_summary, labels)` : grille auto-fit, un SVG par moteur ayant un `aggregated_calibration`. - Rendu strictement server-side, déterministe, **pas de JavaScript**, cohérent avec le SVG du CDD (Sprint 18) et les sections inter-moteurs (Sprint 37) et NER (Sprint 41). - `_build_report_data` expose `aggregated_calibration` par moteur dans `engines_summary`. `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 au moins un moteur a un `aggregated_calibration`** (rapport adaptatif). - +13 clés i18n FR/EN (`h_calibration`, `calibration_note`, `calibration_summary_caption`, `calibration_engine_label`, `calibration_ece_label`, `calibration_mce_label`, `calibration_n_label`, `calibration_acc_label`, `calibration_conf_label`, `calibration_docs_label`, `reliability_diagram_title`, `reliability_x_axis`, `reliability_y_axis`). - +43 tests dans `test_sprint43_calibration_html.py` couvrant le rendu (résumé, SVG avec barres/points/diagonale, grille multi-moteurs), le masquage adaptatif (4 cas dégénérés), l'anti-injection (engine name `