Spaces:
Sleeping
Sleeping
File size: 9,439 Bytes
e88e70e 979f3c3 e88e70e 979f3c3 e88e70e 2d6c41d e88e70e 43d25a5 e88e70e 2d6c41d e88e70e 43d25a5 e88e70e 2d6c41d e88e70e 979f3c3 e88e70e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | """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 = [
'<table style="border-collapse:collapse;width:100%;'
'font-size:.9rem;margin-bottom:.8rem">',
'<thead><tr>',
]
for col in (h_engine, h_total, h_n_types, h_worst):
parts.append(
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
f'border-bottom:1px solid #ccc;font-weight:600">'
f'{_e(col)}</th>'
)
parts.append("</tr></thead><tbody>")
# 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'<tr>'
f'<td style="padding:.4rem .6rem">{_e(str(engine))}</td>'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'background:{color};font-family:monospace;font-weight:600">'
f'{deficit * 100:+.2f}</td>'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'font-family:monospace">{n_types}</td>'
f'<td style="padding:.4rem .6rem">{worst_str}</td>'
f'</tr>'
)
parts.append("</tbody></table>")
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 = [
'<table style="border-collapse:collapse;width:100%;'
'font-size:.9rem">',
'<thead><tr>',
]
for col in (h_engine, h_deg_type, h_n_docs,
h_n_with_data, h_deficit, h_above):
parts.append(
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
f'border-bottom:1px solid #ccc;font-weight:600">'
f'{_e(col)}</th>'
)
parts.append("</tr></thead><tbody>")
# 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'<td style="padding:.4rem .6rem;text-align:right;'
f'background:{color};font-family:monospace">'
f'{deficit_str}</td>'
)
else:
deficit_cell = (
'<td style="padding:.4rem .6rem;text-align:right;'
'opacity:.4">—</td>'
)
parts.append(
f'<tr>'
f'<td style="padding:.4rem .6rem">{_e(str(engine))}</td>'
f'<td style="padding:.4rem .6rem">{_e(str(deg_type))}</td>'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'font-family:monospace">{n_docs}</td>'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'font-family:monospace">{n_with_data}</td>'
f'{deficit_cell}'
f'<td style="padding:.4rem .6rem;text-align:right;'
f'font-family:monospace">{n_above}</td>'
f'</tr>'
)
parts.append("</tbody></table>")
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 = [
'<section class="robproj-section" style="margin:1.5rem 0">',
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.7rem">'
f'{_e(note)}</div>',
]
if summary_table:
parts.append(
f'<div style="font-weight:600;margin:.4rem 0 .3rem 0">'
f'{_e(h_summary)}</div>'
)
parts.append(summary_table)
if detail_table:
parts.append(
f'<div style="font-weight:600;margin:.6rem 0 .3rem 0">'
f'{_e(h_detail)}</div>'
)
parts.append(detail_table)
parts.append('</section>')
return "".join(parts)
__all__ = ["build_robustness_projection_html"]
|