"""Rendu HTML « Déficit projeté de robustesse » — Sprint 88
(A.I.8 vue HTML).
Suite directe ``picarones/core/robustness_projection.py``
(Sprint 81). Pattern identique aux autres rendus : server-
side, pas de JS, anti-injection systématique.
Note d'intégration
------------------
La robustesse synthétique (``picarones.measurements.robustness``) est
exécutée par la CLI ``picarones robustness`` indépendamment du
benchmark principal. Pour produire la vue de projection,
l'utilisateur compose :
.. code-block:: python
from picarones.measurements.robustness import analyze_robustness
from picarones.measurements.robustness_projection import (
project_robustness_on_corpus,
aggregate_projection_per_engine,
)
from picarones.report.robustness_projection_render import (
build_robustness_projection_html,
)
rob = analyze_robustness(corpus, [engine]) # Sprint 8
projection = project_robustness_on_corpus(
rob.curves,
[doc.image_quality.as_dict() for doc in benchmark.docs],
) # Sprint 81
aggregated = aggregate_projection_per_engine(projection)
html = build_robustness_projection_html(
projection, aggregated, labels,
)
Vue
---
1. **Tableau résumé par moteur** : déficit total attendu,
nombre de types de dégradation, pire dégradation.
2. **Tableau détaillé par couple (moteur × dégradation)** :
docs, docs avec data, déficit, % docs au-dessus du seuil
critique.
Les cellules « déficit » sont colorées par gradient vert
(faible) → orange → rouge (≥ 5 points de CER projetés).
Adaptive : ``""`` si la projection est vide (aucune courbe ou
aucun document avec qualité).
"""
from __future__ import annotations
from html import escape as _e
from typing import Optional
from picarones.report.render_helpers import color_traffic_light
def _build_summary_table(
aggregated: dict,
labels: dict[str, str],
) -> str:
if not aggregated:
return ""
h_engine = labels.get("robproj_engine", "Moteur")
h_total = labels.get("robproj_total", "Déficit total (pts CER)")
h_n_types = labels.get("robproj_n_types", "Types évalués")
h_worst = labels.get("robproj_worst", "Pire dégradation")
parts = [
'
',
'',
]
for col in (h_engine, h_total, h_n_types, h_worst):
parts.append(
f'| '
f'{_e(col)} | '
)
parts.append("
")
# Tri par déficit décroissant
rows = sorted(
aggregated.items(),
key=lambda kv: -float(
kv[1].get("total_expected_deficit") or 0.0
),
)
for engine, info in rows:
deficit = float(info.get("total_expected_deficit") or 0.0)
n_types = int(info.get("n_degradation_types") or 0)
worst_type = info.get("worst_degradation_type")
worst_deficit = info.get("worst_degradation_deficit")
color = color_traffic_light(abs(deficit), low_is_good=True, scale_max=0.05)
worst_str = (
f"{_e(str(worst_type))} ({worst_deficit * 100:+.1f})"
if worst_type and isinstance(worst_deficit, (int, float))
else "—"
)
parts.append(
f''
f'| {_e(str(engine))} | '
f''
f'{deficit * 100:+.2f} | '
f'{n_types} | '
f'{worst_str} | '
f'
'
)
parts.append("
")
return "".join(parts)
def _build_detail_table(
projection: dict,
labels: dict[str, str],
) -> str:
if not projection:
return ""
h_engine = labels.get("robproj_engine", "Moteur")
h_deg_type = labels.get("robproj_deg_type", "Dégradation")
h_n_docs = labels.get("robproj_n_docs", "Docs")
h_n_with_data = labels.get("robproj_n_with_data", "Docs avec data")
h_deficit = labels.get("robproj_deficit", "Δ CER projeté (pts)")
h_above = labels.get("robproj_above", "Docs ≥ seuil critique")
parts = [
'',
'',
]
for col in (h_engine, h_deg_type, h_n_docs,
h_n_with_data, h_deficit, h_above):
parts.append(
f'| '
f'{_e(col)} | '
)
parts.append("
")
# Tri stable : par moteur puis type de dégradation
for engine in sorted(projection):
per_type = projection[engine] or {}
for deg_type in sorted(per_type):
entry = per_type[deg_type] or {}
n_docs = int(entry.get("n_docs") or 0)
n_with_data = int(entry.get("n_docs_with_data") or 0)
deficit = entry.get("deficit_vs_baseline")
n_above = int(entry.get("n_docs_above_critical") or 0)
if isinstance(deficit, (int, float)):
color = color_traffic_light(abs(float(deficit)), low_is_good=True, scale_max=0.05)
deficit_str = f"{float(deficit) * 100:+.2f}"
deficit_cell = (
f''
f'{deficit_str} | '
)
else:
deficit_cell = (
'— | '
)
parts.append(
f''
f'| {_e(str(engine))} | '
f'{_e(str(deg_type))} | '
f'{n_docs} | '
f'{n_with_data} | '
f'{deficit_cell}'
f'{n_above} | '
f'
'
)
parts.append("
")
return "".join(parts)
def build_robustness_projection_html(
projection: Optional[dict],
aggregated: Optional[dict] = None,
labels: Optional[dict[str, str]] = None,
) -> str:
"""Construit la vue HTML « Déficit projeté de robustesse ».
Parameters
----------
projection:
Sortie de ``project_robustness_on_corpus`` (Sprint 81),
forme ``{engine: {deg_type: {...}}}``. Si ``None`` ou
vide, retourne ``""``.
aggregated:
Sortie de ``aggregate_projection_per_engine`` (Sprint
81). Si ``None``, sera calculé à partir de
``projection``.
labels:
Dict i18n. Clés sous le préfixe ``robproj_*``.
Returns
-------
str
Section HTML, ou ``""`` si projection vide.
"""
if not projection:
return ""
if aggregated is None:
from picarones.measurements.robustness_projection import (
aggregate_projection_per_engine,
)
aggregated = aggregate_projection_per_engine(projection)
labels = labels or {}
title = labels.get(
"robproj_title",
"Déficit projeté de robustesse sur le corpus réel",
)
note = labels.get(
"robproj_note",
"Projection des courbes de dégradation synthétique sur "
"les caractéristiques d'image réelles. Le déficit total "
"suppose l'indépendance des dégradations — c'est une "
"approximation utile pour le diagnostic, pas un verdict.",
)
summary_table = _build_summary_table(aggregated or {}, labels)
detail_table = _build_detail_table(projection, labels)
if not summary_table and not detail_table:
return ""
h_summary = labels.get("robproj_summary", "Résumé par moteur")
h_detail = labels.get(
"robproj_detail", "Détail par couple (moteur × dégradation)",
)
parts = [
'',
f'{_e(title)}
',
f''
f'{_e(note)}
',
]
if summary_table:
parts.append(
f''
f'{_e(h_summary)}
'
)
parts.append(summary_table)
if detail_table:
parts.append(
f''
f'{_e(h_detail)}
'
)
parts.append(detail_table)
parts.append('')
return "".join(parts)
__all__ = ["build_robustness_projection_html"]