"""Câblage runner du delta Flesch (Sprint 87 — A.II.2). Sprint 87 — A.II.2 (vue HTML + câblage runner du delta Flesch livré par le Sprint 52). Pourquoi ce module ------------------ Le ``flesch_delta`` mesure la différence de lisibilité entre la GT et la sortie OCR. Un score positif signale une *over- normalisation* typique des LLM/VLM qui modernisent un texte ancien (le Flesch monte parce que les mots sont plus simples) ; un score négatif signale une dégradation OCR brutale. Cette métrique est calculée **automatiquement** par le runner sur chaque document, agrégée par moteur, et présentée dans le rapport. Adaptive masking ---------------- On ne calcule que si la GT contient ≥ 5 mots — en dessous, le Flesch est trop instable pour être informatif. Langue ------ Lecture depuis ``corpus.metadata.get("language", "fr")``. Pour les corpus mixtes, l'utilisateur peut passer une langue explicite à l'orchestrateur. """ from __future__ import annotations import logging import statistics from typing import Iterable, Optional from picarones.measurements.readability import ( Language, count_words, flesch_delta, flesch_score, ) logger = logging.getLogger(__name__) _MIN_WORDS_FOR_FLESCH = 5 def compute_readability_metrics( reference: Optional[str], hypothesis: Optional[str], *, lang: Language = "fr", ) -> Optional[dict]: """Calcule le delta Flesch d'un document avec adaptive masking. Retourne ``None`` si la GT contient moins de ``_MIN_WORDS_FOR_FLESCH`` mots. """ ref = reference or "" n_ref_words = count_words(ref) if n_ref_words < _MIN_WORDS_FOR_FLESCH: return None hyp = hypothesis or "" flesch_ref = flesch_score(ref, lang=lang) flesch_hyp = flesch_score(hyp, lang=lang) if hyp else None delta = ( flesch_delta(ref, hyp, lang=lang) if hyp else None ) return { "lang": lang, "flesch_reference": flesch_ref, "flesch_hypothesis": flesch_hyp, "flesch_delta": delta, "n_words_reference": n_ref_words, } def aggregate_readability_metrics( per_doc: Iterable[Optional[dict]], ) -> Optional[dict]: """Agrège : moyenne/médiane des deltas + part de docs « over-normalisés » (delta > +5 points). """ docs = [d for d in per_doc if d] if not docs: return None deltas = [ float(d["flesch_delta"]) for d in docs if isinstance(d.get("flesch_delta"), (int, float)) ] if not deltas: return None over_norm = sum(1 for d in deltas if d > 5.0) under_norm = sum(1 for d in deltas if d < -5.0) lang = docs[0].get("lang") or "fr" return { "lang": lang, "n_docs": len(docs), "n_docs_with_delta": len(deltas), "delta_mean": statistics.fmean(deltas), "delta_median": statistics.median(deltas), "delta_min": min(deltas), "delta_max": max(deltas), "n_over_normalized": over_norm, "n_under_normalized": under_norm, "over_normalized_rate": over_norm / len(deltas), } __all__ = [ "compute_readability_metrics", "aggregate_readability_metrics", ]