"""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'' ) 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'' f'' f'' f'' f'' ) parts.append("
' f'{_e(col)}
{_e(str(engine))}' f'{deficit * 100:+.2f}{n_types}{worst_str}
") 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'' ) 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'' ) else: deficit_cell = ( '' ) parts.append( f'' f'' f'' f'' f'' f'{deficit_cell}' f'' f'' ) parts.append("
' f'{_e(col)}
' f'{deficit_str}
{_e(str(engine))}{_e(str(deg_type))}{n_docs}{n_with_data}{n_above}
") 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"]