Spaces:
Sleeping
Sleeping
File size: 7,260 Bytes
6fef74e 2d6c41d 6fef74e 43d25a5 6fef74e 2d6c41d 6fef74e | 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 | """Rendu HTML server-side de la vue stratifiée par script_type (Sprint 46).
Suite directe du Sprint 45 (couche backend). Affiche le classement
moteur par strate sous forme de tableaux pliables (HTML ``<details>``,
pas de JavaScript).
- ``build_stratified_ranking_html`` — un ``<details>`` par strate avec
tableau ``moteur, médiane, moyenne, docs``. Cellule médiane colorée
par gradient vert (faible CER) → rouge (CER élevé).
Principe : cohérent avec ``inter_engine_render``, ``ner_render`` et
``calibration_render`` — server-side, déterministe, pas de JS.
Masquage adaptatif : la fonction retourne ``""`` si aucune strate
n'est disponible (``available_strata`` vide).
Anti-injection : tous les noms de moteurs et de strates sont passés
à ``html.escape``.
"""
from __future__ import annotations
from html import escape as _e
from typing import Optional
from picarones.report.render_helpers import color_traffic_light
def _format_cer(cer: Optional[float]) -> str:
if cer is None:
return "—"
return f"{cer * 100:.2f} %"
def build_stratified_ranking_html(
stratified_ranking: Optional[dict],
available_strata: Optional[list],
homogeneity: Optional[dict] = None,
labels: Optional[dict[str, str]] = None,
) -> str:
"""Construit la section HTML stratifiée.
Parameters
----------
stratified_ranking:
``{stratum: [ranking_entry, …]}`` produit par
``BenchmarkResult.stratified_ranking()``.
available_strata:
Liste triée des strates (``BenchmarkResult.available_strata()``).
homogeneity:
Dict produit par ``BenchmarkResult.corpus_homogeneity()`` si
disponible — sert à afficher l'écart inter-strate du leader
en tête de section.
labels:
i18n. Fallback FR si manquantes.
Returns
-------
str
HTML ``<div>...</div>`` ou ``""`` si stratification absente.
"""
if not stratified_ranking or not available_strata:
return ""
labels = labels or {}
caption = labels.get(
"stratification_caption",
"Classement par strate (script_type)",
)
description = labels.get(
"stratification_description",
"Le tableau global classe sur l'ensemble du corpus. Quand le "
"corpus est hétérogène, certains moteurs dominent sur un type "
"de document et perdent sur un autre — la vue stratifiée le "
"révèle.",
)
engine_label = labels.get("col_engine", "Moteur")
median_label = labels.get("stratification_median_label", "Médiane CER")
mean_label = labels.get("stratification_mean_label", "Moyenne CER")
docs_label = labels.get("stratification_docs_label", "Documents")
no_data = labels.get("stratification_no_data_label", "—")
n_docs_in_stratum_label = labels.get(
"stratification_n_docs_label", "documents",
)
parts: list[str] = []
parts.append('<div class="stratified-ranking" style="margin-top:1.2rem">')
parts.append(
f'<h3 style="margin:0 0 .3rem 0">{_e(caption)}</h3>'
)
parts.append(
f'<div style="font-size:.78rem;color:var(--text-muted);'
f'margin-bottom:.6rem">{_e(description)}</div>'
)
# Bandeau d'hétérogénéité si disponible
if homogeneity and homogeneity.get("max_inter_strata_gap") is not None:
gap = float(homogeneity["max_inter_strata_gap"])
leader = str(homogeneity.get("leader") or "")
min_strat, max_strat = homogeneity.get(
"leader_max_gap_strata", ["", ""]
)
gap_template = labels.get(
"stratification_gap_summary",
"Écart inter-strate du leader {leader} : {gap_pct} points "
"de CER médian (entre « {min_stratum} » et « {max_stratum} »).",
)
gap_text = gap_template.format(
leader=leader,
gap_pct=f"{gap * 100:.1f}",
min_stratum=min_strat,
max_stratum=max_strat,
)
# gap_text contient déjà des données utilisateur — on n'échappe pas
# le template lui-même (i18n connue), mais on n'injecte pas non plus
# de markup. _e() est appliqué aux variables via format() côté template.
parts.append(
f'<div style="font-size:.82rem;background:#fff8e1;'
f'border-left:3px solid #f9a825;padding:.4rem .6rem;'
f'margin-bottom:.6rem">⚠ {_e(gap_text)}</div>'
)
# Une ``<details>`` par strate (premier ouvert pour donner le contexte)
for i, stratum in enumerate(available_strata):
entries = stratified_ranking.get(stratum) or []
n_docs_total = max((int(e.get("documents") or 0) for e in entries), default=0)
open_attr = " open" if i == 0 else ""
parts.append(
f'<details class="stratum-block"{open_attr} '
f'style="margin-bottom:.4rem;border:1px solid var(--border);'
f'border-radius:6px;padding:.4rem .6rem">'
)
parts.append(
f'<summary style="cursor:pointer;font-weight:600">'
f'{_e(stratum)} '
f'<span style="font-weight:400;color:var(--text-muted);'
f'font-size:.85rem">({n_docs_total} {_e(n_docs_in_stratum_label)})</span>'
f'</summary>'
)
parts.append(
'<table style="border-collapse:collapse;font-size:.85rem;'
'margin-top:.4rem;width:100%">'
)
parts.append("<thead><tr>")
for hdr in (engine_label, median_label, mean_label, docs_label):
parts.append(
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
f'border-bottom:1px solid var(--border);font-weight:600">'
f'{_e(hdr)}</th>'
)
parts.append("</tr></thead><tbody>")
for entry in entries:
engine = str(entry.get("engine", ""))
median = entry.get("median_cer")
mean = entry.get("mean_cer")
n_docs = int(entry.get("documents") or 0)
bg = color_traffic_light(float(median), low_is_good=True, scale_max=0.30) if median is not None else "#f4f4f4"
parts.append("<tr>")
parts.append(
f'<td style="padding:.3rem .5rem;font-weight:600">'
f'{_e(engine)}</td>'
)
parts.append(
f'<td style="padding:.3rem .5rem;background:{bg};'
f'font-variant-numeric:tabular-nums">'
f'{_e(_format_cer(median)) if median is not None else _e(no_data)}'
f'</td>'
)
parts.append(
f'<td style="padding:.3rem .5rem;'
f'font-variant-numeric:tabular-nums">'
f'{_e(_format_cer(mean)) if mean is not None else _e(no_data)}'
f'</td>'
)
parts.append(
f'<td style="padding:.3rem .5rem;'
f'font-variant-numeric:tabular-nums">{n_docs}</td>'
)
parts.append("</tr>")
parts.append("</tbody></table>")
parts.append("</details>")
parts.append("</div>")
return "".join(parts)
__all__ = [
"build_stratified_ranking_html",
]
|