Picarones / picarones /measurements /readability_hooks.py
Claude
refactor(measurements): renommer *_runner.py en *_hooks.py pour cohérence
77d9c47 unverified
Raw
History Blame
3.19 kB
"""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",
]