"""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'| {_e(h_mean)} | '
f'{_e(h_median)} | '
f'{_e(h_min)} | '
f'{_e(h_max)} | '
f'{_e(h_stdev)} | '
f'{_e(h_docs)} | '
f'
'
f''
f'| '
f'{mean:.3f} | '
f'{median:.3f} | '
f'{mn:.3f} | '
f'{mx:.3f} | '
f'{sd:.3f} | '
f'{n_docs} | '
f'
'
)
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'| '
f'{_e(col)} | '
)
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'| {_e(feat_label)} | '
f'{feat_mean:.3f} | '
f'{feat_stdev:.3f} | '
f''
f'{feat_norm:.3f} | '
f'
'
)
parts.append("
")
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"]