"""Rendu HTML « Profil d'image du corpus » — Sprint 93 (A.II.7). Suite directe ``picarones/core/image_predictive.py``. Pattern identique aux autres rendus : server-side, pas de JS, anti- injection systématique. Vue --- Deux blocs dans une section unique : 1. **Complexité paléographique** : moyenne, médiane, min, max, écart-type sur l'ensemble du corpus. 2. **Homogénéité du corpus** : score combiné + détail par feature (mean, stdev, contribution normalisée). Adaptive : ``""`` si pas de données. Note d'intégration ------------------ Module pur — l'utilisateur compose : .. code-block:: python from picarones.measurements.image_predictive import aggregate_corpus_predictive from picarones.report.image_predictive_render import ( build_image_predictive_html, ) qualities = [doc.image_quality.as_dict() for doc in benchmark.docs] agg = aggregate_corpus_predictive(qualities) html = build_image_predictive_html(agg, labels) """ from __future__ import annotations from html import escape as _e from typing import Optional from picarones.report.render_helpers import color_traffic_light _FEATURE_LABEL_KEYS = { "noise_level": "imgpred_feat_noise", "sharpness_score": "imgpred_feat_sharpness", "contrast_score": "imgpred_feat_contrast", "rotation_degrees": "imgpred_feat_rotation", } def _render_complexity_block( aggregated: dict, labels: dict[str, str], ) -> str: h_complex = labels.get( "imgpred_complexity", "Complexité paléographique", ) h_mean = labels.get("imgpred_mean", "Moyenne") h_median = labels.get("imgpred_median", "Médiane") h_min = labels.get("imgpred_min", "Min") h_max = labels.get("imgpred_max", "Max") h_stdev = labels.get("imgpred_stdev", "Écart-type") h_docs = labels.get("imgpred_docs", "Docs") mean = float(aggregated.get("complexity_mean") or 0.0) median = float(aggregated.get("complexity_median") or 0.0) mn = float(aggregated.get("complexity_min") or 0.0) mx = float(aggregated.get("complexity_max") or 0.0) sd = float(aggregated.get("complexity_stdev") or 0.0) n_docs = int(aggregated.get("n_docs") or 0) color_mean = color_traffic_light(mean, low_is_good=True) return ( f'
' f'{_e(h_complex)}
' '' f'' f'' f'' f'' f'' f'' f'' f'' f'' f'' f'' f'' f'' f'' f'' f'
{_e(h_mean)}{_e(h_median)}{_e(h_min)}{_e(h_max)}{_e(h_stdev)}{_e(h_docs)}
' f'{mean:.3f}{median:.3f}{mn:.3f}{mx:.3f}{sd:.3f}{n_docs}
' ) def _render_homogeneity_block( homogeneity: dict, labels: dict[str, str], ) -> str: h_homo = labels.get( "imgpred_homogeneity", "Homogénéité du corpus", ) h_feat = labels.get("imgpred_feature", "Feature") h_mean = labels.get("imgpred_feat_mean", "Moyenne") h_stdev = labels.get("imgpred_feat_stdev", "Écart-type") h_norm = labels.get( "imgpred_feat_norm", "Contribution normalisée", ) score = float(homogeneity.get("score") or 0.0) color = color_traffic_light(score, low_is_good=True) parts = [ f'
' f'{_e(h_homo)} : ' f'{score:.3f}' f'
', '', '', ] for col in (h_feat, h_mean, h_stdev, h_norm): parts.append( f'' ) parts.append("") per_feat = homogeneity.get("per_feature") or {} for key, label_key in _FEATURE_LABEL_KEYS.items(): if key not in per_feat: continue slot = per_feat[key] feat_label = labels.get(label_key, key) feat_mean = float(slot.get("mean") or 0.0) feat_stdev = float(slot.get("stdev") or 0.0) feat_norm = float(slot.get("normalised") or 0.0) norm_color = color_traffic_light(feat_norm, low_is_good=True) parts.append( f'' f'' f'' f'' f'' f'' ) parts.append("
' f'{_e(col)}
{_e(feat_label)}{feat_mean:.3f}{feat_stdev:.3f}' f'{feat_norm:.3f}
") return "".join(parts) def build_image_predictive_html( aggregated: Optional[dict], labels: Optional[dict[str, str]] = None, ) -> str: """Construit la vue HTML « Profil d'image du corpus ». Parameters ---------- aggregated: Sortie de ``aggregate_corpus_predictive``. Si ``None`` ou ``n_docs == 0``, retourne ``""``. labels: Dict i18n. Clés sous le préfixe ``imgpred_*``. """ if not aggregated: return "" if not aggregated.get("n_docs"): return "" labels = labels or {} title = labels.get( "imgpred_title", "Profil d'image du corpus", ) note = labels.get( "imgpred_note", "Score de complexité paléographique combinant bruit, " "flou, faible contraste et rotation. Le score " "d'homogénéité signale si la moyenne globale est fiable " "(corpus uniforme) ou trompeuse (corpus hétérogène — " "voir alors la vue stratifiée).", ) parts = [ '
', f'

{_e(title)}

', f'
' f'{_e(note)}
', ] parts.append(_render_complexity_block(aggregated, labels)) homo = aggregated.get("homogeneity") if isinstance(homo, dict): parts.append(_render_homogeneity_block(homo, labels)) parts.append("
") return "".join(parts) __all__ = ["build_image_predictive_html"]