Spaces:
Sleeping
feat(migration): Phase 5.C batch 3 — 5 renderers moyens vers reports_v2/html/
Browse filesTroisième vague de migration des 22 renderers thématiques. Tous
les renderers sélectionnés sont purs sur le contrat : import
depuis ``_helpers/`` uniquement, pas de dépendance sur des modules
legacy non-migrés.
Migrations effectuées
---------------------
| Source legacy | Destination canonique |
|------------------------------------------------|--------------------------------------------------------|
| ``report/module_audit_render.py`` (173) | ``reports_v2/html/renderers/module_audit.py`` |
| ``report/incremental_comparison_render.py`` (201)| ``reports_v2/html/renderers/incremental_comparison.py``|
| ``report/image_predictive_render.py`` (207) | ``reports_v2/html/renderers/image_predictive.py`` |
| ``report/error_absorption_render.py`` (210) | ``reports_v2/html/renderers/error_absorption.py`` |
| ``report/ner_render.py`` (222) | ``reports_v2/html/renderers/ner.py`` |
Total : ~1013 lignes relocalisées. 5 nouveaux shims minimaux
(< 20 lignes) avec ``DeprecationWarning``.
Cumul Phase 5.C (batches 1+2+3) : 15 / 22 renderers migrés
(~2211 lignes). 7 renderers restants.
Acceptance
----------
5019 tests passent, lint vert, architecture vérifiée.
https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP
- docs/migration/legacy-retirement-plan.md +31 -6
- picarones/report/error_absorption_render.py +11 -203
- picarones/report/generator.py +1 -1
- picarones/report/image_predictive_render.py +11 -200
- picarones/report/incremental_comparison_render.py +11 -194
- picarones/report/module_audit_render.py +11 -166
- picarones/report/ner_render.py +11 -215
- picarones/report/views/diagnostics.py +1 -1
- picarones/report/views/pipeline.py +3 -3
- picarones/reports_v2/html/renderers/error_absorption.py +216 -0
- picarones/reports_v2/html/renderers/image_predictive.py +213 -0
- picarones/reports_v2/html/renderers/incremental_comparison.py +207 -0
- picarones/reports_v2/html/renderers/module_audit.py +179 -0
- picarones/reports_v2/html/renderers/ner.py +227 -0
- tests/integration/test_sprint94_error_absorption.py +1 -1
- tests/measurements/test_sprint93_image_predictive.py +1 -1
- tests/measurements/test_sprint96_incremental_comparison.py +1 -1
- tests/measurements/test_sprint97_module_policy.py +1 -1
- tests/report/test_sprint41_ner_html.py +1 -1
|
@@ -692,12 +692,7 @@ architecture vérifiée.
|
|
| 692 |
**Reporté aux batches suivants** :
|
| 693 |
|
| 694 |
- Batch 2 ✅ (cf. ci-dessous) — 5 renderers (45-165 LOC).
|
| 695 |
-
- Batch 3 (
|
| 696 |
-
``ner``, ``image_predictive``,
|
| 697 |
-
``incremental_comparison``, ``numerical_sequences``
|
| 698 |
-
(ce dernier exige d'abord la migration du module
|
| 699 |
-
``measurements/numerical_sequences.py`` qui dépend de
|
| 700 |
-
``measurements/roman_numerals.py``).
|
| 701 |
- Batch 4 (~5 renderers gros) : ``error_absorption``,
|
| 702 |
``baseline``, ``inter_engine``, ``robustness_projection``,
|
| 703 |
``stratification``.
|
|
@@ -752,6 +747,36 @@ architecture vérifiée.
|
|
| 752 |
**Cumul Phase 5.C** (batches 1+2) : 10 / 22 renderers migrés
|
| 753 |
(~1198 lignes). 12 renderers restants.
|
| 754 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
### Phase 6 — Pipelines OCR+LLM (`pipelines/`)
|
| 756 |
|
| 757 |
**Modules** : `pipelines/base.OCRLLMPipeline` (3 modes), `pipelines/
|
|
|
|
| 692 |
**Reporté aux batches suivants** :
|
| 693 |
|
| 694 |
- Batch 2 ✅ (cf. ci-dessous) — 5 renderers (45-165 LOC).
|
| 695 |
+
- Batch 3 ✅ (cf. ci-dessous) — 5 renderers (173-222 LOC).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
- Batch 4 (~5 renderers gros) : ``error_absorption``,
|
| 697 |
``baseline``, ``inter_engine``, ``robustness_projection``,
|
| 698 |
``stratification``.
|
|
|
|
| 747 |
**Cumul Phase 5.C** (batches 1+2) : 10 / 22 renderers migrés
|
| 748 |
(~1198 lignes). 12 renderers restants.
|
| 749 |
|
| 750 |
+
#### Phase 5.C.batch3 — Lot 3 : 5 renderers moyens (2026-05)
|
| 751 |
+
|
| 752 |
+
Troisième vague. Tous les renderers sélectionnés sont
|
| 753 |
+
**purs sur le contrat** : import depuis ``_helpers/`` uniquement,
|
| 754 |
+
pas de dépendance sur des modules legacy non-migrés.
|
| 755 |
+
|
| 756 |
+
**Migrations effectuées** :
|
| 757 |
+
|
| 758 |
+
| Source legacy | Destination canonique |
|
| 759 |
+
|------------------------------------------------|--------------------------------------------------------|
|
| 760 |
+
| ``report/module_audit_render.py`` (173) | ``reports_v2/html/renderers/module_audit.py`` |
|
| 761 |
+
| ``report/incremental_comparison_render.py`` (201)| ``reports_v2/html/renderers/incremental_comparison.py``|
|
| 762 |
+
| ``report/image_predictive_render.py`` (207) | ``reports_v2/html/renderers/image_predictive.py`` |
|
| 763 |
+
| ``report/error_absorption_render.py`` (210) | ``reports_v2/html/renderers/error_absorption.py`` |
|
| 764 |
+
| ``report/ner_render.py`` (222) | ``reports_v2/html/renderers/ner.py`` |
|
| 765 |
+
|
| 766 |
+
Total : ~1013 lignes relocalisées. 5 nouveaux shims minimaux
|
| 767 |
+
(< 20 lignes) avec ``DeprecationWarning``.
|
| 768 |
+
|
| 769 |
+
**Adaptations transverses** :
|
| 770 |
+
|
| 771 |
+
- Tests + ``picarones/report/generator.py`` mis à jour pour les
|
| 772 |
+
5 chemins canoniques.
|
| 773 |
+
|
| 774 |
+
**Acceptance batch 3** : 5019 tests passent, lint vert,
|
| 775 |
+
architecture vérifiée.
|
| 776 |
+
|
| 777 |
+
**Cumul Phase 5.C** (batches 1+2+3) : 15 / 22 renderers migrés
|
| 778 |
+
(~2211 lignes). 7 renderers restants.
|
| 779 |
+
|
| 780 |
### Phase 6 — Pipelines OCR+LLM (`pipelines/`)
|
| 781 |
|
| 782 |
**Modules** : `pipelines/base.OCRLLMPipeline` (3 modes), `pipelines/
|
|
@@ -1,210 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
injection systématique.
|
| 6 |
-
|
| 7 |
-
Vue
|
| 8 |
-
---
|
| 9 |
-
Tableau résumé des jonctions du pipeline ; chaque ligne décrit
|
| 10 |
-
un module post-correction et présente :
|
| 11 |
-
|
| 12 |
-
- erreurs en entrée vs en sortie ;
|
| 13 |
-
- nb corrigées (gradient vert), nb introduites (gradient rouge) ;
|
| 14 |
-
- taux de correction (gradient vert), taux d'introduction
|
| 15 |
-
(gradient rouge) ;
|
| 16 |
-
- amélioration nette (n_corrected - n_introduced) — coloré.
|
| 17 |
-
- éventuellement un échantillon de tokens corrigés/introduits.
|
| 18 |
-
|
| 19 |
-
Adaptive : ``""`` si la liste est vide.
|
| 20 |
-
|
| 21 |
-
Note d'intégration
|
| 22 |
-
------------------
|
| 23 |
-
Module pur — la liste ``junctions`` est composée par
|
| 24 |
-
l'utilisateur depuis son benchmark de pipeline composée :
|
| 25 |
-
|
| 26 |
-
.. code-block:: python
|
| 27 |
-
|
| 28 |
-
from picarones.measurements.error_absorption import (
|
| 29 |
-
compute_error_absorption, aggregate_error_absorption,
|
| 30 |
-
)
|
| 31 |
-
from picarones.report.error_absorption_render import (
|
| 32 |
-
build_error_absorption_html,
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
junctions = []
|
| 36 |
-
for step in pipeline.steps_with_text_output:
|
| 37 |
-
per_doc = [
|
| 38 |
-
compute_error_absorption(doc.gt_text, doc.before_text,
|
| 39 |
-
doc.after_text)
|
| 40 |
-
for doc in benchmark.docs
|
| 41 |
-
]
|
| 42 |
-
agg = aggregate_error_absorption(per_doc)
|
| 43 |
-
if agg is not None:
|
| 44 |
-
agg["junction_name"] = step.name
|
| 45 |
-
junctions.append(agg)
|
| 46 |
-
html = build_error_absorption_html(junctions, labels)
|
| 47 |
"""
|
| 48 |
|
| 49 |
from __future__ import annotations
|
| 50 |
|
| 51 |
-
|
| 52 |
-
from typing import Optional
|
| 53 |
-
|
| 54 |
-
from picarones.reports_v2._helpers.render_helpers import color_diverging, color_traffic_light
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# Palette « net improvement » : vert clair au centre, vert profond
|
| 58 |
-
# si favorable (net > 0), rouge si défavorable (net < 0). Centrée
|
| 59 |
-
# sur le vert clair car un delta nul est déjà « pas de régression ».
|
| 60 |
-
_NET_NEUTRAL_RGB = (167, 240, 167)
|
| 61 |
-
_NET_POSITIVE_RGB = (90, 200, 90)
|
| 62 |
-
_NET_NEGATIVE_RGB = (220, 50, 50)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def build_error_absorption_html(
|
| 66 |
-
junctions: Optional[list],
|
| 67 |
-
labels: Optional[dict[str, str]] = None,
|
| 68 |
-
*,
|
| 69 |
-
sample_max: int = 8,
|
| 70 |
-
) -> str:
|
| 71 |
-
"""Construit la vue HTML « Absorption d'erreur ».
|
| 72 |
-
|
| 73 |
-
Parameters
|
| 74 |
-
----------
|
| 75 |
-
junctions:
|
| 76 |
-
Liste de dicts (un par jonction de pipeline), enrichis
|
| 77 |
-
d'un ``junction_name``. Si vide ou ``None``, retourne
|
| 78 |
-
``""``.
|
| 79 |
-
labels:
|
| 80 |
-
Dict i18n. Clés sous le préfixe ``absorption_*``.
|
| 81 |
-
sample_max:
|
| 82 |
-
Nombre maximal de tokens corrigés/introduits affichés
|
| 83 |
-
en cellule échantillon.
|
| 84 |
-
"""
|
| 85 |
-
if not junctions:
|
| 86 |
-
return ""
|
| 87 |
-
rows = [
|
| 88 |
-
j for j in junctions
|
| 89 |
-
if isinstance(j, dict) and j.get("junction_name")
|
| 90 |
-
]
|
| 91 |
-
if not rows:
|
| 92 |
-
return ""
|
| 93 |
-
labels = labels or {}
|
| 94 |
-
title = labels.get(
|
| 95 |
-
"absorption_title", "Absorption d'erreur par jonction",
|
| 96 |
-
)
|
| 97 |
-
note = labels.get(
|
| 98 |
-
"absorption_note",
|
| 99 |
-
"À chaque jonction du pipeline, deux flux sont mesurés "
|
| 100 |
-
"indépendamment : combien d'erreurs sont corrigées et "
|
| 101 |
-
"combien sont introduites. Une jonction qui corrige "
|
| 102 |
-
"beaucoup mais introduit aussi beaucoup absorbe les "
|
| 103 |
-
"différences amont au lieu de les améliorer.",
|
| 104 |
-
)
|
| 105 |
-
h_junction = labels.get("absorption_junction", "Jonction")
|
| 106 |
-
h_errors_before = labels.get("absorption_errors_before", "Erreurs avant")
|
| 107 |
-
h_errors_after = labels.get("absorption_errors_after", "Erreurs après")
|
| 108 |
-
h_corrected = labels.get("absorption_corrected", "Corrigées")
|
| 109 |
-
h_introduced = labels.get("absorption_introduced", "Introduites")
|
| 110 |
-
h_corr_rate = labels.get("absorption_corr_rate", "% corrigées")
|
| 111 |
-
h_intro_rate = labels.get("absorption_intro_rate", "% introduites")
|
| 112 |
-
h_net = labels.get("absorption_net", "Amélioration nette")
|
| 113 |
-
h_sample = labels.get("absorption_sample", "Échantillon (intro)")
|
| 114 |
-
|
| 115 |
-
# Saturation pour le gradient « net »
|
| 116 |
-
max_abs_net = max(
|
| 117 |
-
(abs(int(r.get("net_improvement") or 0)) for r in rows), default=1,
|
| 118 |
-
) or 1
|
| 119 |
-
|
| 120 |
-
parts = [
|
| 121 |
-
'<section class="absorption-section" style="margin:1rem 0">',
|
| 122 |
-
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 123 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 124 |
-
f'{_e(note)}</div>',
|
| 125 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 126 |
-
'font-size:.9rem">',
|
| 127 |
-
'<thead><tr>',
|
| 128 |
-
]
|
| 129 |
-
for col in (h_junction, h_errors_before, h_errors_after,
|
| 130 |
-
h_corrected, h_introduced, h_corr_rate,
|
| 131 |
-
h_intro_rate, h_net, h_sample):
|
| 132 |
-
parts.append(
|
| 133 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 134 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 135 |
-
f'{_e(col)}</th>'
|
| 136 |
-
)
|
| 137 |
-
parts.append("</tr></thead><tbody>")
|
| 138 |
-
for entry in rows:
|
| 139 |
-
name = str(entry.get("junction_name") or "?")
|
| 140 |
-
n_eb = int(entry.get("n_errors_before") or 0)
|
| 141 |
-
n_ea = int(entry.get("n_errors_after") or 0)
|
| 142 |
-
n_corr = int(entry.get("n_corrected") or 0)
|
| 143 |
-
n_intro = int(entry.get("n_introduced") or 0)
|
| 144 |
-
net = int(entry.get("net_improvement") or 0)
|
| 145 |
-
corr_rate = entry.get("correction_rate")
|
| 146 |
-
intro_rate = entry.get("introduction_rate")
|
| 147 |
-
if isinstance(corr_rate, (int, float)):
|
| 148 |
-
corr_rate_str = f"{corr_rate * 100:.1f}%"
|
| 149 |
-
corr_color = color_traffic_light(float(corr_rate))
|
| 150 |
-
corr_cell = (
|
| 151 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 152 |
-
f'background:{corr_color};font-family:monospace;'
|
| 153 |
-
f'font-weight:600">{corr_rate_str}</td>'
|
| 154 |
-
)
|
| 155 |
-
else:
|
| 156 |
-
corr_cell = (
|
| 157 |
-
'<td style="padding:.4rem .6rem;text-align:right;'
|
| 158 |
-
'opacity:.4">—</td>'
|
| 159 |
-
)
|
| 160 |
-
if isinstance(intro_rate, (int, float)):
|
| 161 |
-
intro_rate_str = f"{intro_rate * 100:.1f}%"
|
| 162 |
-
intro_color = color_traffic_light(float(intro_rate), low_is_good=True)
|
| 163 |
-
intro_cell = (
|
| 164 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 165 |
-
f'background:{intro_color};font-family:monospace;'
|
| 166 |
-
f'font-weight:600">{intro_rate_str}</td>'
|
| 167 |
-
)
|
| 168 |
-
else:
|
| 169 |
-
intro_cell = (
|
| 170 |
-
'<td style="padding:.4rem .6rem;text-align:right;'
|
| 171 |
-
'opacity:.4">—</td>'
|
| 172 |
-
)
|
| 173 |
-
net_color = color_diverging(
|
| 174 |
-
float(net),
|
| 175 |
-
max_abs=float(max_abs_net) if max_abs_net else 1.0,
|
| 176 |
-
neutral_rgb=_NET_NEUTRAL_RGB,
|
| 177 |
-
positive_rgb=_NET_POSITIVE_RGB,
|
| 178 |
-
negative_rgb=_NET_NEGATIVE_RGB,
|
| 179 |
-
)
|
| 180 |
-
intro_sample = entry.get("introduced_tokens_sample") or []
|
| 181 |
-
sample_cell_text = ", ".join(
|
| 182 |
-
_e(str(t)) for t in intro_sample[:sample_max]
|
| 183 |
-
) or "—"
|
| 184 |
-
if len(intro_sample) > sample_max:
|
| 185 |
-
sample_cell_text += " …"
|
| 186 |
-
parts.append(
|
| 187 |
-
f'<tr>'
|
| 188 |
-
f'<td style="padding:.4rem .6rem">{_e(name)}</td>'
|
| 189 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 190 |
-
f'font-family:monospace">{n_eb}</td>'
|
| 191 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 192 |
-
f'font-family:monospace">{n_ea}</td>'
|
| 193 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 194 |
-
f'font-family:monospace">{n_corr}</td>'
|
| 195 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 196 |
-
f'font-family:monospace">{n_intro}</td>'
|
| 197 |
-
f'{corr_cell}'
|
| 198 |
-
f'{intro_cell}'
|
| 199 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 200 |
-
f'background:{net_color};font-family:monospace;'
|
| 201 |
-
f'font-weight:600">{net:+d}</td>'
|
| 202 |
-
f'<td style="padding:.4rem .6rem;font-family:monospace;'
|
| 203 |
-
f'font-size:.8rem">{sample_cell_text}</td>'
|
| 204 |
-
f'</tr>'
|
| 205 |
-
)
|
| 206 |
-
parts.append("</tbody></table></section>")
|
| 207 |
-
return "".join(parts)
|
| 208 |
|
|
|
|
| 209 |
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.error_absorption_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.error_absorption`.
|
| 4 |
+
Phase 5.C du retrait du legacy.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import warnings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
from picarones.reports_v2.html.renderers.error_absorption import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.error_absorption_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.error_absorption instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -268,7 +268,7 @@ class ReportGenerator:
|
|
| 268 |
build_oracle_gap_html,
|
| 269 |
)
|
| 270 |
# Sprint 41 — section NER (résumé F1 par moteur + heatmap par catégorie).
|
| 271 |
-
from picarones.
|
| 272 |
build_ner_per_category_html,
|
| 273 |
build_ner_summary_html,
|
| 274 |
)
|
|
|
|
| 268 |
build_oracle_gap_html,
|
| 269 |
)
|
| 270 |
# Sprint 41 — section NER (résumé F1 par moteur + heatmap par catégorie).
|
| 271 |
+
from picarones.reports_v2.html.renderers.ner import (
|
| 272 |
build_ner_per_category_html,
|
| 273 |
build_ner_summary_html,
|
| 274 |
)
|
|
@@ -1,207 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
injection systématique.
|
| 6 |
-
|
| 7 |
-
Vue
|
| 8 |
-
---
|
| 9 |
-
Deux blocs dans une section unique :
|
| 10 |
-
|
| 11 |
-
1. **Complexité paléographique** : moyenne, médiane, min, max,
|
| 12 |
-
écart-type sur l'ensemble du corpus.
|
| 13 |
-
2. **Homogénéité du corpus** : score combiné + détail par
|
| 14 |
-
feature (mean, stdev, contribution normalisée).
|
| 15 |
-
|
| 16 |
-
Adaptive : ``""`` si pas de données.
|
| 17 |
-
|
| 18 |
-
Note d'intégration
|
| 19 |
-
------------------
|
| 20 |
-
Module pur — l'utilisateur compose :
|
| 21 |
-
|
| 22 |
-
.. code-block:: python
|
| 23 |
-
|
| 24 |
-
from picarones.measurements.image_predictive import aggregate_corpus_predictive
|
| 25 |
-
from picarones.report.image_predictive_render import (
|
| 26 |
-
build_image_predictive_html,
|
| 27 |
-
)
|
| 28 |
-
|
| 29 |
-
qualities = [doc.image_quality.as_dict() for doc in benchmark.docs]
|
| 30 |
-
agg = aggregate_corpus_predictive(qualities)
|
| 31 |
-
html = build_image_predictive_html(agg, labels)
|
| 32 |
"""
|
| 33 |
|
| 34 |
from __future__ import annotations
|
| 35 |
|
| 36 |
-
|
| 37 |
-
from typing import Optional
|
| 38 |
-
|
| 39 |
-
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
_FEATURE_LABEL_KEYS = {
|
| 43 |
-
"noise_level": "imgpred_feat_noise",
|
| 44 |
-
"sharpness_score": "imgpred_feat_sharpness",
|
| 45 |
-
"contrast_score": "imgpred_feat_contrast",
|
| 46 |
-
"rotation_degrees": "imgpred_feat_rotation",
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def _render_complexity_block(
|
| 51 |
-
aggregated: dict, labels: dict[str, str],
|
| 52 |
-
) -> str:
|
| 53 |
-
h_complex = labels.get(
|
| 54 |
-
"imgpred_complexity", "Complexité paléographique",
|
| 55 |
-
)
|
| 56 |
-
h_mean = labels.get("imgpred_mean", "Moyenne")
|
| 57 |
-
h_median = labels.get("imgpred_median", "Médiane")
|
| 58 |
-
h_min = labels.get("imgpred_min", "Min")
|
| 59 |
-
h_max = labels.get("imgpred_max", "Max")
|
| 60 |
-
h_stdev = labels.get("imgpred_stdev", "Écart-type")
|
| 61 |
-
h_docs = labels.get("imgpred_docs", "Docs")
|
| 62 |
-
mean = float(aggregated.get("complexity_mean") or 0.0)
|
| 63 |
-
median = float(aggregated.get("complexity_median") or 0.0)
|
| 64 |
-
mn = float(aggregated.get("complexity_min") or 0.0)
|
| 65 |
-
mx = float(aggregated.get("complexity_max") or 0.0)
|
| 66 |
-
sd = float(aggregated.get("complexity_stdev") or 0.0)
|
| 67 |
-
n_docs = int(aggregated.get("n_docs") or 0)
|
| 68 |
-
color_mean = color_traffic_light(mean, low_is_good=True)
|
| 69 |
-
return (
|
| 70 |
-
f'<div style="font-weight:600;margin:.4rem 0 .3rem 0">'
|
| 71 |
-
f'{_e(h_complex)}</div>'
|
| 72 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 73 |
-
'font-size:.9rem;margin-bottom:.8rem">'
|
| 74 |
-
f'<thead><tr>'
|
| 75 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 76 |
-
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_mean)}</th>'
|
| 77 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 78 |
-
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_median)}</th>'
|
| 79 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 80 |
-
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_min)}</th>'
|
| 81 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 82 |
-
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_max)}</th>'
|
| 83 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 84 |
-
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_stdev)}</th>'
|
| 85 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 86 |
-
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_docs)}</th>'
|
| 87 |
-
f'</tr></thead>'
|
| 88 |
-
f'<tbody><tr>'
|
| 89 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 90 |
-
f'background:{color_mean};font-family:monospace;font-weight:600">'
|
| 91 |
-
f'{mean:.3f}</td>'
|
| 92 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 93 |
-
f'font-family:monospace">{median:.3f}</td>'
|
| 94 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 95 |
-
f'font-family:monospace">{mn:.3f}</td>'
|
| 96 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 97 |
-
f'font-family:monospace">{mx:.3f}</td>'
|
| 98 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 99 |
-
f'font-family:monospace">{sd:.3f}</td>'
|
| 100 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 101 |
-
f'font-family:monospace">{n_docs}</td>'
|
| 102 |
-
f'</tr></tbody></table>'
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def _render_homogeneity_block(
|
| 107 |
-
homogeneity: dict, labels: dict[str, str],
|
| 108 |
-
) -> str:
|
| 109 |
-
h_homo = labels.get(
|
| 110 |
-
"imgpred_homogeneity", "Homogénéité du corpus",
|
| 111 |
-
)
|
| 112 |
-
h_feat = labels.get("imgpred_feature", "Feature")
|
| 113 |
-
h_mean = labels.get("imgpred_feat_mean", "Moyenne")
|
| 114 |
-
h_stdev = labels.get("imgpred_feat_stdev", "Écart-type")
|
| 115 |
-
h_norm = labels.get(
|
| 116 |
-
"imgpred_feat_norm", "Contribution normalisée",
|
| 117 |
-
)
|
| 118 |
-
score = float(homogeneity.get("score") or 0.0)
|
| 119 |
-
color = color_traffic_light(score, low_is_good=True)
|
| 120 |
-
parts = [
|
| 121 |
-
f'<div style="font-weight:600;margin:.4rem 0 .3rem 0">'
|
| 122 |
-
f'{_e(h_homo)} : '
|
| 123 |
-
f'<span style="background:{color};padding:.1rem .4rem;'
|
| 124 |
-
f'border-radius:.3rem;font-family:monospace">{score:.3f}</span>'
|
| 125 |
-
f'</div>',
|
| 126 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 127 |
-
'font-size:.9rem">',
|
| 128 |
-
'<thead><tr>',
|
| 129 |
-
]
|
| 130 |
-
for col in (h_feat, h_mean, h_stdev, h_norm):
|
| 131 |
-
parts.append(
|
| 132 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 133 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 134 |
-
f'{_e(col)}</th>'
|
| 135 |
-
)
|
| 136 |
-
parts.append("</tr></thead><tbody>")
|
| 137 |
-
per_feat = homogeneity.get("per_feature") or {}
|
| 138 |
-
for key, label_key in _FEATURE_LABEL_KEYS.items():
|
| 139 |
-
if key not in per_feat:
|
| 140 |
-
continue
|
| 141 |
-
slot = per_feat[key]
|
| 142 |
-
feat_label = labels.get(label_key, key)
|
| 143 |
-
feat_mean = float(slot.get("mean") or 0.0)
|
| 144 |
-
feat_stdev = float(slot.get("stdev") or 0.0)
|
| 145 |
-
feat_norm = float(slot.get("normalised") or 0.0)
|
| 146 |
-
norm_color = color_traffic_light(feat_norm, low_is_good=True)
|
| 147 |
-
parts.append(
|
| 148 |
-
f'<tr>'
|
| 149 |
-
f'<td style="padding:.4rem .6rem">{_e(feat_label)}</td>'
|
| 150 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 151 |
-
f'font-family:monospace">{feat_mean:.3f}</td>'
|
| 152 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 153 |
-
f'font-family:monospace">{feat_stdev:.3f}</td>'
|
| 154 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 155 |
-
f'background:{norm_color};font-family:monospace">'
|
| 156 |
-
f'{feat_norm:.3f}</td>'
|
| 157 |
-
f'</tr>'
|
| 158 |
-
)
|
| 159 |
-
parts.append("</tbody></table>")
|
| 160 |
-
return "".join(parts)
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
def build_image_predictive_html(
|
| 164 |
-
aggregated: Optional[dict],
|
| 165 |
-
labels: Optional[dict[str, str]] = None,
|
| 166 |
-
) -> str:
|
| 167 |
-
"""Construit la vue HTML « Profil d'image du corpus ».
|
| 168 |
-
|
| 169 |
-
Parameters
|
| 170 |
-
----------
|
| 171 |
-
aggregated:
|
| 172 |
-
Sortie de ``aggregate_corpus_predictive``. Si ``None``
|
| 173 |
-
ou ``n_docs == 0``, retourne ``""``.
|
| 174 |
-
labels:
|
| 175 |
-
Dict i18n. Clés sous le préfixe ``imgpred_*``.
|
| 176 |
-
"""
|
| 177 |
-
if not aggregated:
|
| 178 |
-
return ""
|
| 179 |
-
if not aggregated.get("n_docs"):
|
| 180 |
-
return ""
|
| 181 |
-
labels = labels or {}
|
| 182 |
-
title = labels.get(
|
| 183 |
-
"imgpred_title", "Profil d'image du corpus",
|
| 184 |
-
)
|
| 185 |
-
note = labels.get(
|
| 186 |
-
"imgpred_note",
|
| 187 |
-
"Score de complexité paléographique combinant bruit, "
|
| 188 |
-
"flou, faible contraste et rotation. Le score "
|
| 189 |
-
"d'homogénéité signale si la moyenne globale est fiable "
|
| 190 |
-
"(corpus uniforme) ou trompeuse (corpus hétérogène — "
|
| 191 |
-
"voir alors la vue stratifiée).",
|
| 192 |
-
)
|
| 193 |
-
parts = [
|
| 194 |
-
'<section class="imgpred-section" style="margin:1rem 0">',
|
| 195 |
-
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 196 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 197 |
-
f'{_e(note)}</div>',
|
| 198 |
-
]
|
| 199 |
-
parts.append(_render_complexity_block(aggregated, labels))
|
| 200 |
-
homo = aggregated.get("homogeneity")
|
| 201 |
-
if isinstance(homo, dict):
|
| 202 |
-
parts.append(_render_homogeneity_block(homo, labels))
|
| 203 |
-
parts.append("</section>")
|
| 204 |
-
return "".join(parts)
|
| 205 |
|
|
|
|
| 206 |
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.image_predictive_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.image_predictive`.
|
| 4 |
+
Phase 5.C du retrait du legacy.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import warnings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
from picarones.reports_v2.html.renderers.image_predictive import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.image_predictive_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.image_predictive instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -1,201 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
anti-injection systématique.
|
| 6 |
-
|
| 7 |
-
Vue
|
| 8 |
-
---
|
| 9 |
-
Tableau ANOVA-like : pour chaque valeur du slot variant, mean
|
| 10 |
-
± stdev, rang moyen, n_observations. Mean colorée en
|
| 11 |
-
gradient vert (meilleur) → rouge (pire), normalisée sur la
|
| 12 |
-
plage des moyennes observées.
|
| 13 |
-
|
| 14 |
-
Adaptive : ``""`` si ``analysis`` est ``None``.
|
| 15 |
-
|
| 16 |
-
Note d'intégration
|
| 17 |
-
------------------
|
| 18 |
-
Module pur — l'utilisateur compose :
|
| 19 |
-
|
| 20 |
-
.. code-block:: python
|
| 21 |
-
|
| 22 |
-
from picarones.measurements.incremental_comparison import (
|
| 23 |
-
PipelineRun, compare_isolated_effect,
|
| 24 |
-
)
|
| 25 |
-
from picarones.report.incremental_comparison_render import (
|
| 26 |
-
build_incremental_comparison_html,
|
| 27 |
-
)
|
| 28 |
-
|
| 29 |
-
runs = [
|
| 30 |
-
PipelineRun(name=p.name,
|
| 31 |
-
slots={"ocr": p.ocr, "llm": p.llm},
|
| 32 |
-
score=p.cer_mean)
|
| 33 |
-
for p in benchmark.pipelines
|
| 34 |
-
]
|
| 35 |
-
analysis = compare_isolated_effect(runs, "llm")
|
| 36 |
-
html = build_incremental_comparison_html(analysis, labels)
|
| 37 |
"""
|
| 38 |
|
| 39 |
from __future__ import annotations
|
| 40 |
|
| 41 |
-
|
| 42 |
-
from typing import Optional
|
| 43 |
-
|
| 44 |
-
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def _bg_for_relative_score(
|
| 48 |
-
score: float, low: float, high: float, higher_is_better: bool,
|
| 49 |
-
) -> str:
|
| 50 |
-
"""Mappe ``score`` sur une plage [low, high] et retourne une cellule
|
| 51 |
-
colorée traffic-light.
|
| 52 |
-
|
| 53 |
-
Si ``higher_is_better=True``, ``score=high`` est vert ; sinon
|
| 54 |
-
``score=low`` est vert.
|
| 55 |
-
"""
|
| 56 |
-
if high == low:
|
| 57 |
-
return color_traffic_light(1.0) # neutre vert clair
|
| 58 |
-
return color_traffic_light(
|
| 59 |
-
score,
|
| 60 |
-
low_is_good=not higher_is_better,
|
| 61 |
-
scale_min=low,
|
| 62 |
-
scale_max=high,
|
| 63 |
-
)
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def _format_score(value: Optional[float]) -> str:
|
| 67 |
-
if value is None:
|
| 68 |
-
return "—"
|
| 69 |
-
if abs(value) < 1.0:
|
| 70 |
-
return f"{value * 100:.2f}%"
|
| 71 |
-
return f"{value:.3f}"
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def build_incremental_comparison_html(
|
| 75 |
-
analysis: Optional[dict],
|
| 76 |
-
labels: Optional[dict[str, str]] = None,
|
| 77 |
-
) -> str:
|
| 78 |
-
"""Construit la vue HTML « Comparaison contrôlée ».
|
| 79 |
-
|
| 80 |
-
Parameters
|
| 81 |
-
----------
|
| 82 |
-
analysis:
|
| 83 |
-
Sortie de ``compare_isolated_effect``. ``None`` ou
|
| 84 |
-
``per_value`` vide → retourne ``""``.
|
| 85 |
-
labels:
|
| 86 |
-
Dict i18n. Clés sous le préfixe ``incr_*``.
|
| 87 |
-
"""
|
| 88 |
-
if not analysis:
|
| 89 |
-
return ""
|
| 90 |
-
per_value = analysis.get("per_value") or {}
|
| 91 |
-
if not per_value:
|
| 92 |
-
return ""
|
| 93 |
-
labels = labels or {}
|
| 94 |
-
title = labels.get(
|
| 95 |
-
"incr_title", "Comparaison contrôlée par slot",
|
| 96 |
-
)
|
| 97 |
-
note = labels.get(
|
| 98 |
-
"incr_note",
|
| 99 |
-
"Effet isolé du module variant sur les pipelines en "
|
| 100 |
-
"contrôlant les autres slots. Pour chaque valeur du "
|
| 101 |
-
"slot, moyenne ± écart-type, rang moyen sur les groupes "
|
| 102 |
-
"fixes, et nombre d'observations. Type design "
|
| 103 |
-
"d'expérience pour des comparaisons honnêtes.",
|
| 104 |
-
)
|
| 105 |
-
slot_label = labels.get("incr_slot_label", "Slot variant")
|
| 106 |
-
h_value = labels.get("incr_value", "Valeur")
|
| 107 |
-
h_mean = labels.get("incr_mean", "Score moyen")
|
| 108 |
-
h_stdev = labels.get("incr_stdev", "± σ")
|
| 109 |
-
h_rank = labels.get("incr_rank", "Rang moyen")
|
| 110 |
-
h_n_obs = labels.get("incr_n_obs", "Observations")
|
| 111 |
-
h_groups = labels.get("incr_groups", "Groupes fixes")
|
| 112 |
-
higher_is_better = bool(analysis.get("higher_is_better", False))
|
| 113 |
-
|
| 114 |
-
# Plage de moyennes pour le code couleur
|
| 115 |
-
means = [
|
| 116 |
-
d["mean"] for d in per_value.values() if d.get("mean") is not None
|
| 117 |
-
]
|
| 118 |
-
low = min(means) if means else 0.0
|
| 119 |
-
high = max(means) if means else 0.0
|
| 120 |
-
|
| 121 |
-
varying_slot = str(analysis.get("varying_slot") or "?")
|
| 122 |
-
n_groups = int(analysis.get("n_groups") or 0)
|
| 123 |
-
n_runs = int(analysis.get("n_runs") or 0)
|
| 124 |
-
best = analysis.get("best_value")
|
| 125 |
-
worst = analysis.get("worst_value")
|
| 126 |
-
|
| 127 |
-
parts = [
|
| 128 |
-
'<section class="incr-section" style="margin:1rem 0">',
|
| 129 |
-
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 130 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 131 |
-
f'{_e(note)}</div>',
|
| 132 |
-
f'<div style="font-size:.85rem;margin-bottom:.5rem">'
|
| 133 |
-
f'<strong>{_e(slot_label)} :</strong> '
|
| 134 |
-
f'<code>{_e(varying_slot)}</code> '
|
| 135 |
-
f'<span style="opacity:.75">'
|
| 136 |
-
f'{n_runs} runs, {n_groups} {_e(h_groups.lower())}'
|
| 137 |
-
f'</span></div>',
|
| 138 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 139 |
-
'font-size:.9rem">',
|
| 140 |
-
'<thead><tr>',
|
| 141 |
-
]
|
| 142 |
-
for col in (h_value, h_mean, h_stdev, h_rank, h_n_obs):
|
| 143 |
-
parts.append(
|
| 144 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 145 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 146 |
-
f'{_e(col)}</th>'
|
| 147 |
-
)
|
| 148 |
-
parts.append("</tr></thead><tbody>")
|
| 149 |
-
|
| 150 |
-
# Tri par rang moyen ascendant
|
| 151 |
-
rows = sorted(
|
| 152 |
-
per_value.items(),
|
| 153 |
-
key=lambda kv: (kv[1].get("mean_rank") or float("inf")),
|
| 154 |
-
)
|
| 155 |
-
for value, d in rows:
|
| 156 |
-
mean = d.get("mean")
|
| 157 |
-
stdev = d.get("stdev")
|
| 158 |
-
rank = d.get("mean_rank")
|
| 159 |
-
n_obs = int(d.get("n_observations") or 0)
|
| 160 |
-
if isinstance(mean, (int, float)):
|
| 161 |
-
color = _bg_for_relative_score(
|
| 162 |
-
float(mean), low, high, higher_is_better,
|
| 163 |
-
)
|
| 164 |
-
mean_cell = (
|
| 165 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 166 |
-
f'background:{color};font-family:monospace;'
|
| 167 |
-
f'font-weight:600">{_format_score(mean)}</td>'
|
| 168 |
-
)
|
| 169 |
-
else:
|
| 170 |
-
mean_cell = (
|
| 171 |
-
'<td style="padding:.4rem .6rem;text-align:right;'
|
| 172 |
-
'opacity:.4">—</td>'
|
| 173 |
-
)
|
| 174 |
-
stdev_str = (
|
| 175 |
-
f"± {_format_score(stdev)}"
|
| 176 |
-
if isinstance(stdev, (int, float)) else "—"
|
| 177 |
-
)
|
| 178 |
-
rank_str = f"{rank:.2f}" if isinstance(rank, (int, float)) else "—"
|
| 179 |
-
marker = ""
|
| 180 |
-
if value == best:
|
| 181 |
-
marker = ' <span style="color:#16a34a">★</span>'
|
| 182 |
-
elif value == worst:
|
| 183 |
-
marker = ' <span style="color:#dc2626">▼</span>'
|
| 184 |
-
parts.append(
|
| 185 |
-
f'<tr>'
|
| 186 |
-
f'<td style="padding:.4rem .6rem;font-family:monospace">'
|
| 187 |
-
f'{_e(str(value))}{marker}</td>'
|
| 188 |
-
f'{mean_cell}'
|
| 189 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 190 |
-
f'font-family:monospace">{stdev_str}</td>'
|
| 191 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 192 |
-
f'font-family:monospace">{rank_str}</td>'
|
| 193 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 194 |
-
f'font-family:monospace">{n_obs}</td>'
|
| 195 |
-
f'</tr>'
|
| 196 |
-
)
|
| 197 |
-
parts.append("</tbody></table></section>")
|
| 198 |
-
return "".join(parts)
|
| 199 |
|
|
|
|
| 200 |
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.incremental_comparison_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.incremental_comparison`.
|
| 4 |
+
Phase 5.C du retrait du legacy.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import warnings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
from picarones.reports_v2.html.renderers.incremental_comparison import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.incremental_comparison_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.incremental_comparison instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -1,173 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
injection systématique.
|
| 6 |
-
|
| 7 |
-
Vue
|
| 8 |
-
---
|
| 9 |
-
Tableau récapitulatif des modules utilisés dans une pipeline
|
| 10 |
-
composée, chacun avec :
|
| 11 |
-
|
| 12 |
-
- Statut d'audit (✓ vert si tous les checks passent, ✗ rouge
|
| 13 |
-
sinon, avec compte des échecs) ;
|
| 14 |
-
- Métadonnées : version, auteur, licence ;
|
| 15 |
-
- Citation académique si fournie ;
|
| 16 |
-
- Lien vers la homepage si fourni.
|
| 17 |
-
|
| 18 |
-
Adaptive : ``""`` si la liste est vide.
|
| 19 |
-
|
| 20 |
-
Note d'intégration
|
| 21 |
-
------------------
|
| 22 |
-
Module pur — l'utilisateur compose la liste depuis sa
|
| 23 |
-
``PipelineSpec`` augmentée des ``ModuleManifest`` :
|
| 24 |
-
|
| 25 |
-
.. code-block:: python
|
| 26 |
-
|
| 27 |
-
from picarones.measurements.module_policy import audit_module
|
| 28 |
-
from picarones.report.module_audit_render import build_module_audit_html
|
| 29 |
-
|
| 30 |
-
audits = []
|
| 31 |
-
for step in pipeline.steps:
|
| 32 |
-
manifest = step.module.manifest # convention applicative
|
| 33 |
-
result = audit_module(step.module, manifest)
|
| 34 |
-
audits.append({
|
| 35 |
-
"manifest": manifest.as_dict(),
|
| 36 |
-
"audit": result.as_dict(),
|
| 37 |
-
})
|
| 38 |
-
html = build_module_audit_html(audits, labels)
|
| 39 |
"""
|
| 40 |
|
| 41 |
from __future__ import annotations
|
| 42 |
|
| 43 |
-
|
| 44 |
-
from typing import Optional
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def _passed_badge(passed: bool, n_failed: int, label_pass: str,
|
| 48 |
-
label_fail: str) -> str:
|
| 49 |
-
if passed:
|
| 50 |
-
return (
|
| 51 |
-
f'<span style="color:#16a34a;font-weight:700">'
|
| 52 |
-
f'✓ {_e(label_pass)}</span>'
|
| 53 |
-
)
|
| 54 |
-
return (
|
| 55 |
-
f'<span style="color:#dc2626;font-weight:700">'
|
| 56 |
-
f'✗ {_e(label_fail)} ({n_failed})</span>'
|
| 57 |
-
)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def build_module_audit_html(
|
| 61 |
-
audits: Optional[list],
|
| 62 |
-
labels: Optional[dict[str, str]] = None,
|
| 63 |
-
) -> str:
|
| 64 |
-
"""Construit la vue HTML « Modules audités ».
|
| 65 |
-
|
| 66 |
-
Parameters
|
| 67 |
-
----------
|
| 68 |
-
audits:
|
| 69 |
-
Liste de dicts ``{"manifest": ManifestDict, "audit":
|
| 70 |
-
AuditResultDict}``. Si vide ou ``None``, retourne ``""``.
|
| 71 |
-
labels:
|
| 72 |
-
Dict i18n. Clés sous le préfixe ``audit_*``.
|
| 73 |
-
"""
|
| 74 |
-
if not audits:
|
| 75 |
-
return ""
|
| 76 |
-
rows = [
|
| 77 |
-
a for a in audits
|
| 78 |
-
if isinstance(a, dict)
|
| 79 |
-
and isinstance(a.get("manifest"), dict)
|
| 80 |
-
and isinstance(a.get("audit"), dict)
|
| 81 |
-
]
|
| 82 |
-
if not rows:
|
| 83 |
-
return ""
|
| 84 |
-
labels = labels or {}
|
| 85 |
-
title = labels.get("audit_title", "Modules audités")
|
| 86 |
-
note = labels.get(
|
| 87 |
-
"audit_note",
|
| 88 |
-
"Récapitulatif des modules utilisés dans la pipeline "
|
| 89 |
-
"composée. Un module qui ne passe pas l'audit n'est "
|
| 90 |
-
"pas exécutable. Métadonnées issues du manifest fourni "
|
| 91 |
-
"par le contributeur (auteur, licence, citation).",
|
| 92 |
-
)
|
| 93 |
-
label_pass = labels.get("audit_pass", "audit OK")
|
| 94 |
-
label_fail = labels.get("audit_fail", "checks échoués")
|
| 95 |
-
h_module = labels.get("audit_module", "Module")
|
| 96 |
-
h_status = labels.get("audit_status", "Audit")
|
| 97 |
-
h_version = labels.get("audit_version", "Version")
|
| 98 |
-
h_author = labels.get("audit_author", "Auteur")
|
| 99 |
-
h_license = labels.get("audit_license", "Licence")
|
| 100 |
-
h_io = labels.get("audit_io", "Entrée → sortie")
|
| 101 |
-
h_citation = labels.get("audit_citation", "Citation")
|
| 102 |
-
h_homepage = labels.get("audit_homepage", "Page projet")
|
| 103 |
-
|
| 104 |
-
parts = [
|
| 105 |
-
'<section class="audit-section" style="margin:1rem 0">',
|
| 106 |
-
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 107 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 108 |
-
f'{_e(note)}</div>',
|
| 109 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 110 |
-
'font-size:.9rem">',
|
| 111 |
-
'<thead><tr>',
|
| 112 |
-
]
|
| 113 |
-
for col in (h_module, h_status, h_version, h_author,
|
| 114 |
-
h_license, h_io, h_citation, h_homepage):
|
| 115 |
-
parts.append(
|
| 116 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 117 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 118 |
-
f'{_e(col)}</th>'
|
| 119 |
-
)
|
| 120 |
-
parts.append("</tr></thead><tbody>")
|
| 121 |
-
|
| 122 |
-
for entry in rows:
|
| 123 |
-
manifest = entry["manifest"]
|
| 124 |
-
audit = entry["audit"]
|
| 125 |
-
name = str(manifest.get("name") or "?")
|
| 126 |
-
version = str(manifest.get("version") or "—")
|
| 127 |
-
author = str(manifest.get("author") or "—")
|
| 128 |
-
license_ = str(manifest.get("license") or "—")
|
| 129 |
-
in_types = ", ".join(manifest.get("input_types") or []) or "—"
|
| 130 |
-
out_types = ", ".join(manifest.get("output_types") or []) or "—"
|
| 131 |
-
citation = manifest.get("citation") or ""
|
| 132 |
-
homepage = manifest.get("homepage") or ""
|
| 133 |
-
passed = bool(audit.get("passed"))
|
| 134 |
-
n_failed = int(audit.get("n_failed") or 0)
|
| 135 |
-
status_cell = _passed_badge(
|
| 136 |
-
passed, n_failed, label_pass, label_fail,
|
| 137 |
-
)
|
| 138 |
-
# Citation : tronqué si trop long
|
| 139 |
-
citation_str = str(citation)[:120]
|
| 140 |
-
if len(str(citation)) > 120:
|
| 141 |
-
citation_str += "…"
|
| 142 |
-
citation_cell = (
|
| 143 |
-
_e(citation_str) if citation_str.strip() else "—"
|
| 144 |
-
)
|
| 145 |
-
# Homepage : on n'auto-link **pas** (anti-injection +
|
| 146 |
-
# honnêteté : l'URL peut pointer ailleurs). On affiche
|
| 147 |
-
# le texte échappé tel quel.
|
| 148 |
-
homepage_cell = (
|
| 149 |
-
_e(str(homepage))[:80] + ("…" if len(str(homepage)) > 80 else "")
|
| 150 |
-
) if str(homepage).strip() else "—"
|
| 151 |
-
parts.append(
|
| 152 |
-
f'<tr>'
|
| 153 |
-
f'<td style="padding:.4rem .6rem;font-family:monospace">'
|
| 154 |
-
f'{_e(name)}</td>'
|
| 155 |
-
f'<td style="padding:.4rem .6rem">{status_cell}</td>'
|
| 156 |
-
f'<td style="padding:.4rem .6rem;font-family:monospace">'
|
| 157 |
-
f'{_e(version)}</td>'
|
| 158 |
-
f'<td style="padding:.4rem .6rem">{_e(author)}</td>'
|
| 159 |
-
f'<td style="padding:.4rem .6rem;font-family:monospace">'
|
| 160 |
-
f'{_e(license_)}</td>'
|
| 161 |
-
f'<td style="padding:.4rem .6rem;font-family:monospace;'
|
| 162 |
-
f'font-size:.8rem">{_e(in_types)} → {_e(out_types)}</td>'
|
| 163 |
-
f'<td style="padding:.4rem .6rem;font-size:.8rem;'
|
| 164 |
-
f'opacity:.85">{citation_cell}</td>'
|
| 165 |
-
f'<td style="padding:.4rem .6rem;font-family:monospace;'
|
| 166 |
-
f'font-size:.8rem">{homepage_cell}</td>'
|
| 167 |
-
f'</tr>'
|
| 168 |
-
)
|
| 169 |
-
parts.append("</tbody></table></section>")
|
| 170 |
-
return "".join(parts)
|
| 171 |
|
|
|
|
| 172 |
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.module_audit_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.module_audit`.
|
| 4 |
+
Phase 5.C du retrait du legacy.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import warnings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
from picarones.reports_v2.html.renderers.module_audit import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.module_audit_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.module_audit instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -1,222 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
blocs HTML qui remontent ces données dans le rapport.
|
| 6 |
-
|
| 7 |
-
- ``build_ner_summary_html`` — encart factuel par moteur : F1 global,
|
| 8 |
-
precision/recall, total entités, hallucinations, missed.
|
| 9 |
-
- ``build_ner_per_category_html`` — table heatmap moteur × catégorie,
|
| 10 |
-
cellules colorées par F1 (rouge → vert).
|
| 11 |
-
|
| 12 |
-
Principe — cohérent avec ``inter_engine_render`` (Sprint 37) : rendu
|
| 13 |
-
server-side, pas de JavaScript, déterministe. Si aucun moteur n'a de
|
| 14 |
-
``aggregated_ner``, les fonctions retournent une chaîne vide — la vue
|
| 15 |
-
est silencieusement omise (rapport adaptatif).
|
| 16 |
-
|
| 17 |
-
Anti-injection : tous les noms de moteurs et catégories sont passés à
|
| 18 |
-
``html.escape`` avant insertion.
|
| 19 |
"""
|
| 20 |
|
| 21 |
from __future__ import annotations
|
| 22 |
|
| 23 |
-
|
| 24 |
-
from typing import Optional
|
| 25 |
-
|
| 26 |
-
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def _engines_with_ner(engines_summary: list[dict]) -> list[dict]:
|
| 30 |
-
"""Filtre les moteurs qui ont une analyse NER agrégée."""
|
| 31 |
-
return [e for e in engines_summary if e.get("aggregated_ner")]
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def build_ner_summary_html(
|
| 35 |
-
engines_summary: list[dict],
|
| 36 |
-
labels: Optional[dict[str, str]] = None,
|
| 37 |
-
) -> str:
|
| 38 |
-
"""Construit l'encart résumé NER : F1 global par moteur + totaux.
|
| 39 |
-
|
| 40 |
-
Parameters
|
| 41 |
-
----------
|
| 42 |
-
engines_summary:
|
| 43 |
-
Liste de dicts moteur (au moins ``name`` et ``aggregated_ner``).
|
| 44 |
-
labels:
|
| 45 |
-
Dict d'étiquettes i18n.
|
| 46 |
-
|
| 47 |
-
Returns
|
| 48 |
-
-------
|
| 49 |
-
str
|
| 50 |
-
HTML ``<div>...</div>`` ou ``""`` si aucun moteur n'a de NER.
|
| 51 |
-
"""
|
| 52 |
-
relevant = _engines_with_ner(engines_summary)
|
| 53 |
-
if not relevant:
|
| 54 |
-
return ""
|
| 55 |
-
|
| 56 |
-
labels = labels or {}
|
| 57 |
-
caption = labels.get("ner_summary_caption", "Précision sur entités nommées")
|
| 58 |
-
engine_label = labels.get("ner_engine_label", "Moteur")
|
| 59 |
-
f1_label = labels.get("ner_f1_label", "F1 global")
|
| 60 |
-
p_label = labels.get("ner_precision_label", "Précision")
|
| 61 |
-
r_label = labels.get("ner_recall_label", "Rappel")
|
| 62 |
-
docs_label = labels.get("ner_doc_count_label", "Docs évalués")
|
| 63 |
-
halluc_label = labels.get("ner_hallucinated_label", "Hallucinations")
|
| 64 |
-
missed_label = labels.get("ner_missed_label", "Entités manquées")
|
| 65 |
-
|
| 66 |
-
parts: list[str] = []
|
| 67 |
-
parts.append('<div class="ner-summary">')
|
| 68 |
-
parts.append(
|
| 69 |
-
f'<div class="ner-summary-caption" style="font-weight:600;'
|
| 70 |
-
f'margin-bottom:.4rem">{_e(caption)}</div>'
|
| 71 |
-
)
|
| 72 |
-
parts.append(
|
| 73 |
-
'<table class="ner-summary-table" '
|
| 74 |
-
'style="border-collapse:collapse;font-size:.85rem;width:100%">'
|
| 75 |
-
)
|
| 76 |
-
parts.append("<thead><tr>")
|
| 77 |
-
for hdr in (engine_label, f1_label, p_label, r_label,
|
| 78 |
-
docs_label, halluc_label, missed_label):
|
| 79 |
-
parts.append(
|
| 80 |
-
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 81 |
-
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 82 |
-
f'{_e(hdr)}</th>'
|
| 83 |
-
)
|
| 84 |
-
parts.append("</tr></thead><tbody>")
|
| 85 |
-
for engine in relevant:
|
| 86 |
-
agg = engine["aggregated_ner"]
|
| 87 |
-
global_stats = agg.get("global", {}) or {}
|
| 88 |
-
f1 = float(global_stats.get("f1") or 0.0)
|
| 89 |
-
precision = float(global_stats.get("precision") or 0.0)
|
| 90 |
-
recall = float(global_stats.get("recall") or 0.0)
|
| 91 |
-
doc_count = int(agg.get("doc_count") or 0)
|
| 92 |
-
hallucinated = int(agg.get("hallucinated_total") or 0)
|
| 93 |
-
missed = int(agg.get("missed_total") or 0)
|
| 94 |
-
bg = color_traffic_light(f1)
|
| 95 |
-
parts.append("<tr>")
|
| 96 |
-
parts.append(
|
| 97 |
-
f'<td style="padding:.3rem .5rem;font-weight:600">'
|
| 98 |
-
f'{_e(engine.get("name", ""))}</td>'
|
| 99 |
-
)
|
| 100 |
-
parts.append(
|
| 101 |
-
f'<td style="padding:.3rem .5rem;background:{bg};'
|
| 102 |
-
f'font-variant-numeric:tabular-nums">{f1 * 100:.1f} %</td>'
|
| 103 |
-
)
|
| 104 |
-
parts.append(
|
| 105 |
-
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 106 |
-
f'{precision * 100:.1f} %</td>'
|
| 107 |
-
)
|
| 108 |
-
parts.append(
|
| 109 |
-
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 110 |
-
f'{recall * 100:.1f} %</td>'
|
| 111 |
-
)
|
| 112 |
-
parts.append(
|
| 113 |
-
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 114 |
-
f'{doc_count}</td>'
|
| 115 |
-
)
|
| 116 |
-
parts.append(
|
| 117 |
-
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 118 |
-
f'{hallucinated}</td>'
|
| 119 |
-
)
|
| 120 |
-
parts.append(
|
| 121 |
-
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 122 |
-
f'{missed}</td>'
|
| 123 |
-
)
|
| 124 |
-
parts.append("</tr>")
|
| 125 |
-
parts.append("</tbody></table></div>")
|
| 126 |
-
return "".join(parts)
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def build_ner_per_category_html(
|
| 130 |
-
engines_summary: list[dict],
|
| 131 |
-
labels: Optional[dict[str, str]] = None,
|
| 132 |
-
) -> str:
|
| 133 |
-
"""Construit la heatmap NER moteur × catégorie d'entité.
|
| 134 |
-
|
| 135 |
-
Lignes = moteurs, colonnes = catégories (PER, LOC, ORG, DATE,
|
| 136 |
-
MISC…). Cellules colorées par F1 (rouge → vert). La cellule
|
| 137 |
-
affiche le F1 en pourcentage. Cellules vides quand la catégorie
|
| 138 |
-
n'a pas été observée pour le moteur.
|
| 139 |
-
|
| 140 |
-
Returns
|
| 141 |
-
-------
|
| 142 |
-
str
|
| 143 |
-
HTML ``<div>...</div>`` ou ``""`` si pas de données.
|
| 144 |
-
"""
|
| 145 |
-
relevant = _engines_with_ner(engines_summary)
|
| 146 |
-
if not relevant:
|
| 147 |
-
return ""
|
| 148 |
-
|
| 149 |
-
# Catégories : union sur tous les moteurs, ordre alphabétique
|
| 150 |
-
all_categories: set[str] = set()
|
| 151 |
-
for engine in relevant:
|
| 152 |
-
per_cat = (engine["aggregated_ner"] or {}).get("per_category") or {}
|
| 153 |
-
all_categories.update(per_cat.keys())
|
| 154 |
-
if not all_categories:
|
| 155 |
-
return ""
|
| 156 |
-
categories = sorted(all_categories)
|
| 157 |
-
|
| 158 |
-
labels = labels or {}
|
| 159 |
-
caption = labels.get(
|
| 160 |
-
"ner_per_category_caption",
|
| 161 |
-
"F1 par catégorie d'entité (heatmap)",
|
| 162 |
-
)
|
| 163 |
-
engine_label = labels.get("ner_engine_label", "Moteur")
|
| 164 |
-
no_data = labels.get("ner_no_data_label", "—")
|
| 165 |
-
|
| 166 |
-
parts: list[str] = []
|
| 167 |
-
parts.append('<div class="ner-per-category">')
|
| 168 |
-
parts.append(
|
| 169 |
-
f'<div class="ner-per-category-caption" '
|
| 170 |
-
f'style="font-weight:600;margin-bottom:.4rem">{_e(caption)}</div>'
|
| 171 |
-
)
|
| 172 |
-
parts.append(
|
| 173 |
-
'<table class="ner-per-category-table" '
|
| 174 |
-
'style="border-collapse:collapse;font-size:.8rem">'
|
| 175 |
-
)
|
| 176 |
-
parts.append("<thead><tr>")
|
| 177 |
-
parts.append(
|
| 178 |
-
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 179 |
-
f'border-bottom:1px solid var(--border)">{_e(engine_label)}</th>'
|
| 180 |
-
)
|
| 181 |
-
for cat in categories:
|
| 182 |
-
parts.append(
|
| 183 |
-
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:center;'
|
| 184 |
-
f'border-bottom:1px solid var(--border)">{_e(cat)}</th>'
|
| 185 |
-
)
|
| 186 |
-
parts.append("</tr></thead><tbody>")
|
| 187 |
-
for engine in relevant:
|
| 188 |
-
per_cat = (engine["aggregated_ner"] or {}).get("per_category") or {}
|
| 189 |
-
parts.append("<tr>")
|
| 190 |
-
parts.append(
|
| 191 |
-
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:right;'
|
| 192 |
-
f'border-right:1px solid var(--border);font-weight:600">'
|
| 193 |
-
f'{_e(engine.get("name", ""))}</th>'
|
| 194 |
-
)
|
| 195 |
-
for cat in categories:
|
| 196 |
-
stats = per_cat.get(cat)
|
| 197 |
-
if not stats or int(stats.get("support", 0)) == 0:
|
| 198 |
-
parts.append(
|
| 199 |
-
f'<td style="padding:.3rem .5rem;text-align:center;'
|
| 200 |
-
f'background:#f4f4f4;color:var(--text-muted);'
|
| 201 |
-
f'font-style:italic">{_e(no_data)}</td>'
|
| 202 |
-
)
|
| 203 |
-
else:
|
| 204 |
-
f1 = float(stats.get("f1") or 0.0)
|
| 205 |
-
support = int(stats.get("support", 0))
|
| 206 |
-
bg = color_traffic_light(f1)
|
| 207 |
-
parts.append(
|
| 208 |
-
f'<td style="padding:.3rem .5rem;text-align:center;'
|
| 209 |
-
f'background:{bg};color:#222;'
|
| 210 |
-
f'font-variant-numeric:tabular-nums" '
|
| 211 |
-
f'title="support={support}">'
|
| 212 |
-
f'{f1 * 100:.1f} %</td>'
|
| 213 |
-
)
|
| 214 |
-
parts.append("</tr>")
|
| 215 |
-
parts.append("</tbody></table></div>")
|
| 216 |
-
return "".join(parts)
|
| 217 |
|
|
|
|
| 218 |
|
| 219 |
-
|
| 220 |
-
"
|
| 221 |
-
"
|
| 222 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.ner_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.ner`. Phase
|
| 4 |
+
5.C du retrait du legacy.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import warnings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
from picarones.reports_v2.html.renderers.ner import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.ner_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.ner instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -144,7 +144,7 @@ def build_diagnostics_view_html(
|
|
| 144 |
from picarones.measurements.image_predictive import (
|
| 145 |
aggregate_corpus_predictive,
|
| 146 |
)
|
| 147 |
-
from picarones.
|
| 148 |
build_image_predictive_html,
|
| 149 |
)
|
| 150 |
aggregated = aggregate_corpus_predictive(image_qualities)
|
|
|
|
| 144 |
from picarones.measurements.image_predictive import (
|
| 145 |
aggregate_corpus_predictive,
|
| 146 |
)
|
| 147 |
+
from picarones.reports_v2.html.renderers.image_predictive import (
|
| 148 |
build_image_predictive_html,
|
| 149 |
)
|
| 150 |
aggregated = aggregate_corpus_predictive(image_qualities)
|
|
@@ -148,7 +148,7 @@ def build_pipeline_view_html(
|
|
| 148 |
# Sous-section 3 : absorption d'erreur par jonction
|
| 149 |
if junctions:
|
| 150 |
try:
|
| 151 |
-
from picarones.
|
| 152 |
build_error_absorption_html,
|
| 153 |
)
|
| 154 |
html = build_error_absorption_html(junctions, labels=labels)
|
|
@@ -171,7 +171,7 @@ def build_pipeline_view_html(
|
|
| 171 |
from picarones.measurements.incremental_comparison import (
|
| 172 |
compare_isolated_effect,
|
| 173 |
)
|
| 174 |
-
from picarones.
|
| 175 |
build_incremental_comparison_html,
|
| 176 |
)
|
| 177 |
comparison = compare_isolated_effect(
|
|
@@ -200,7 +200,7 @@ def build_pipeline_view_html(
|
|
| 200 |
# Sous-section 5 : audit des modules contribués
|
| 201 |
if module_audits:
|
| 202 |
try:
|
| 203 |
-
from picarones.
|
| 204 |
build_module_audit_html,
|
| 205 |
)
|
| 206 |
html = build_module_audit_html(module_audits, labels=labels)
|
|
|
|
| 148 |
# Sous-section 3 : absorption d'erreur par jonction
|
| 149 |
if junctions:
|
| 150 |
try:
|
| 151 |
+
from picarones.reports_v2.html.renderers.error_absorption import (
|
| 152 |
build_error_absorption_html,
|
| 153 |
)
|
| 154 |
html = build_error_absorption_html(junctions, labels=labels)
|
|
|
|
| 171 |
from picarones.measurements.incremental_comparison import (
|
| 172 |
compare_isolated_effect,
|
| 173 |
)
|
| 174 |
+
from picarones.reports_v2.html.renderers.incremental_comparison import (
|
| 175 |
build_incremental_comparison_html,
|
| 176 |
)
|
| 177 |
comparison = compare_isolated_effect(
|
|
|
|
| 200 |
# Sous-section 5 : audit des modules contribués
|
| 201 |
if module_audits:
|
| 202 |
try:
|
| 203 |
+
from picarones.reports_v2.html.renderers.module_audit import (
|
| 204 |
build_module_audit_html,
|
| 205 |
)
|
| 206 |
html = build_module_audit_html(module_audits, labels=labels)
|
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML « Absorption d'erreur » — Sprint 94 (B.3).
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.error_absorption_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.error_absorption``. Le chemin
|
| 6 |
+
legacy reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 7 |
+
suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Suite directe ``picarones/core/error_absorption.py``. Pattern
|
| 10 |
+
identique aux autres rendus : server-side, pas de JS, anti-
|
| 11 |
+
injection systématique.
|
| 12 |
+
|
| 13 |
+
Vue
|
| 14 |
+
---
|
| 15 |
+
Tableau résumé des jonctions du pipeline ; chaque ligne décrit
|
| 16 |
+
un module post-correction et présente :
|
| 17 |
+
|
| 18 |
+
- erreurs en entrée vs en sortie ;
|
| 19 |
+
- nb corrigées (gradient vert), nb introduites (gradient rouge) ;
|
| 20 |
+
- taux de correction (gradient vert), taux d'introduction
|
| 21 |
+
(gradient rouge) ;
|
| 22 |
+
- amélioration nette (n_corrected - n_introduced) — coloré.
|
| 23 |
+
- éventuellement un échantillon de tokens corrigés/introduits.
|
| 24 |
+
|
| 25 |
+
Adaptive : ``""`` si la liste est vide.
|
| 26 |
+
|
| 27 |
+
Note d'intégration
|
| 28 |
+
------------------
|
| 29 |
+
Module pur — la liste ``junctions`` est composée par
|
| 30 |
+
l'utilisateur depuis son benchmark de pipeline composée :
|
| 31 |
+
|
| 32 |
+
.. code-block:: python
|
| 33 |
+
|
| 34 |
+
from picarones.measurements.error_absorption import (
|
| 35 |
+
compute_error_absorption, aggregate_error_absorption,
|
| 36 |
+
)
|
| 37 |
+
from picarones.reports_v2.html.renderers.error_absorption import (
|
| 38 |
+
build_error_absorption_html,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
junctions = []
|
| 42 |
+
for step in pipeline.steps_with_text_output:
|
| 43 |
+
per_doc = [
|
| 44 |
+
compute_error_absorption(doc.gt_text, doc.before_text,
|
| 45 |
+
doc.after_text)
|
| 46 |
+
for doc in benchmark.docs
|
| 47 |
+
]
|
| 48 |
+
agg = aggregate_error_absorption(per_doc)
|
| 49 |
+
if agg is not None:
|
| 50 |
+
agg["junction_name"] = step.name
|
| 51 |
+
junctions.append(agg)
|
| 52 |
+
html = build_error_absorption_html(junctions, labels)
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
from __future__ import annotations
|
| 56 |
+
|
| 57 |
+
from html import escape as _e
|
| 58 |
+
from typing import Optional
|
| 59 |
+
|
| 60 |
+
from picarones.reports_v2._helpers.render_helpers import color_diverging, color_traffic_light
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# Palette « net improvement » : vert clair au centre, vert profond
|
| 64 |
+
# si favorable (net > 0), rouge si défavorable (net < 0). Centrée
|
| 65 |
+
# sur le vert clair car un delta nul est déjà « pas de régression ».
|
| 66 |
+
_NET_NEUTRAL_RGB = (167, 240, 167)
|
| 67 |
+
_NET_POSITIVE_RGB = (90, 200, 90)
|
| 68 |
+
_NET_NEGATIVE_RGB = (220, 50, 50)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def build_error_absorption_html(
|
| 72 |
+
junctions: Optional[list],
|
| 73 |
+
labels: Optional[dict[str, str]] = None,
|
| 74 |
+
*,
|
| 75 |
+
sample_max: int = 8,
|
| 76 |
+
) -> str:
|
| 77 |
+
"""Construit la vue HTML « Absorption d'erreur ».
|
| 78 |
+
|
| 79 |
+
Parameters
|
| 80 |
+
----------
|
| 81 |
+
junctions:
|
| 82 |
+
Liste de dicts (un par jonction de pipeline), enrichis
|
| 83 |
+
d'un ``junction_name``. Si vide ou ``None``, retourne
|
| 84 |
+
``""``.
|
| 85 |
+
labels:
|
| 86 |
+
Dict i18n. Clés sous le préfixe ``absorption_*``.
|
| 87 |
+
sample_max:
|
| 88 |
+
Nombre maximal de tokens corrigés/introduits affichés
|
| 89 |
+
en cellule échantillon.
|
| 90 |
+
"""
|
| 91 |
+
if not junctions:
|
| 92 |
+
return ""
|
| 93 |
+
rows = [
|
| 94 |
+
j for j in junctions
|
| 95 |
+
if isinstance(j, dict) and j.get("junction_name")
|
| 96 |
+
]
|
| 97 |
+
if not rows:
|
| 98 |
+
return ""
|
| 99 |
+
labels = labels or {}
|
| 100 |
+
title = labels.get(
|
| 101 |
+
"absorption_title", "Absorption d'erreur par jonction",
|
| 102 |
+
)
|
| 103 |
+
note = labels.get(
|
| 104 |
+
"absorption_note",
|
| 105 |
+
"À chaque jonction du pipeline, deux flux sont mesurés "
|
| 106 |
+
"indépendamment : combien d'erreurs sont corrigées et "
|
| 107 |
+
"combien sont introduites. Une jonction qui corrige "
|
| 108 |
+
"beaucoup mais introduit aussi beaucoup absorbe les "
|
| 109 |
+
"différences amont au lieu de les améliorer.",
|
| 110 |
+
)
|
| 111 |
+
h_junction = labels.get("absorption_junction", "Jonction")
|
| 112 |
+
h_errors_before = labels.get("absorption_errors_before", "Erreurs avant")
|
| 113 |
+
h_errors_after = labels.get("absorption_errors_after", "Erreurs après")
|
| 114 |
+
h_corrected = labels.get("absorption_corrected", "Corrigées")
|
| 115 |
+
h_introduced = labels.get("absorption_introduced", "Introduites")
|
| 116 |
+
h_corr_rate = labels.get("absorption_corr_rate", "% corrigées")
|
| 117 |
+
h_intro_rate = labels.get("absorption_intro_rate", "% introduites")
|
| 118 |
+
h_net = labels.get("absorption_net", "Amélioration nette")
|
| 119 |
+
h_sample = labels.get("absorption_sample", "Échantillon (intro)")
|
| 120 |
+
|
| 121 |
+
# Saturation pour le gradient « net »
|
| 122 |
+
max_abs_net = max(
|
| 123 |
+
(abs(int(r.get("net_improvement") or 0)) for r in rows), default=1,
|
| 124 |
+
) or 1
|
| 125 |
+
|
| 126 |
+
parts = [
|
| 127 |
+
'<section class="absorption-section" style="margin:1rem 0">',
|
| 128 |
+
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 129 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 130 |
+
f'{_e(note)}</div>',
|
| 131 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 132 |
+
'font-size:.9rem">',
|
| 133 |
+
'<thead><tr>',
|
| 134 |
+
]
|
| 135 |
+
for col in (h_junction, h_errors_before, h_errors_after,
|
| 136 |
+
h_corrected, h_introduced, h_corr_rate,
|
| 137 |
+
h_intro_rate, h_net, h_sample):
|
| 138 |
+
parts.append(
|
| 139 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 140 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 141 |
+
f'{_e(col)}</th>'
|
| 142 |
+
)
|
| 143 |
+
parts.append("</tr></thead><tbody>")
|
| 144 |
+
for entry in rows:
|
| 145 |
+
name = str(entry.get("junction_name") or "?")
|
| 146 |
+
n_eb = int(entry.get("n_errors_before") or 0)
|
| 147 |
+
n_ea = int(entry.get("n_errors_after") or 0)
|
| 148 |
+
n_corr = int(entry.get("n_corrected") or 0)
|
| 149 |
+
n_intro = int(entry.get("n_introduced") or 0)
|
| 150 |
+
net = int(entry.get("net_improvement") or 0)
|
| 151 |
+
corr_rate = entry.get("correction_rate")
|
| 152 |
+
intro_rate = entry.get("introduction_rate")
|
| 153 |
+
if isinstance(corr_rate, (int, float)):
|
| 154 |
+
corr_rate_str = f"{corr_rate * 100:.1f}%"
|
| 155 |
+
corr_color = color_traffic_light(float(corr_rate))
|
| 156 |
+
corr_cell = (
|
| 157 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 158 |
+
f'background:{corr_color};font-family:monospace;'
|
| 159 |
+
f'font-weight:600">{corr_rate_str}</td>'
|
| 160 |
+
)
|
| 161 |
+
else:
|
| 162 |
+
corr_cell = (
|
| 163 |
+
'<td style="padding:.4rem .6rem;text-align:right;'
|
| 164 |
+
'opacity:.4">—</td>'
|
| 165 |
+
)
|
| 166 |
+
if isinstance(intro_rate, (int, float)):
|
| 167 |
+
intro_rate_str = f"{intro_rate * 100:.1f}%"
|
| 168 |
+
intro_color = color_traffic_light(float(intro_rate), low_is_good=True)
|
| 169 |
+
intro_cell = (
|
| 170 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 171 |
+
f'background:{intro_color};font-family:monospace;'
|
| 172 |
+
f'font-weight:600">{intro_rate_str}</td>'
|
| 173 |
+
)
|
| 174 |
+
else:
|
| 175 |
+
intro_cell = (
|
| 176 |
+
'<td style="padding:.4rem .6rem;text-align:right;'
|
| 177 |
+
'opacity:.4">—</td>'
|
| 178 |
+
)
|
| 179 |
+
net_color = color_diverging(
|
| 180 |
+
float(net),
|
| 181 |
+
max_abs=float(max_abs_net) if max_abs_net else 1.0,
|
| 182 |
+
neutral_rgb=_NET_NEUTRAL_RGB,
|
| 183 |
+
positive_rgb=_NET_POSITIVE_RGB,
|
| 184 |
+
negative_rgb=_NET_NEGATIVE_RGB,
|
| 185 |
+
)
|
| 186 |
+
intro_sample = entry.get("introduced_tokens_sample") or []
|
| 187 |
+
sample_cell_text = ", ".join(
|
| 188 |
+
_e(str(t)) for t in intro_sample[:sample_max]
|
| 189 |
+
) or "—"
|
| 190 |
+
if len(intro_sample) > sample_max:
|
| 191 |
+
sample_cell_text += " …"
|
| 192 |
+
parts.append(
|
| 193 |
+
f'<tr>'
|
| 194 |
+
f'<td style="padding:.4rem .6rem">{_e(name)}</td>'
|
| 195 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 196 |
+
f'font-family:monospace">{n_eb}</td>'
|
| 197 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 198 |
+
f'font-family:monospace">{n_ea}</td>'
|
| 199 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 200 |
+
f'font-family:monospace">{n_corr}</td>'
|
| 201 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 202 |
+
f'font-family:monospace">{n_intro}</td>'
|
| 203 |
+
f'{corr_cell}'
|
| 204 |
+
f'{intro_cell}'
|
| 205 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 206 |
+
f'background:{net_color};font-family:monospace;'
|
| 207 |
+
f'font-weight:600">{net:+d}</td>'
|
| 208 |
+
f'<td style="padding:.4rem .6rem;font-family:monospace;'
|
| 209 |
+
f'font-size:.8rem">{sample_cell_text}</td>'
|
| 210 |
+
f'</tr>'
|
| 211 |
+
)
|
| 212 |
+
parts.append("</tbody></table></section>")
|
| 213 |
+
return "".join(parts)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
__all__ = ["build_error_absorption_html"]
|
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML « Profil d'image du corpus » — Sprint 93 (A.II.7).
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.image_predictive_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.image_predictive``. Le chemin
|
| 6 |
+
legacy reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 7 |
+
suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Suite directe ``picarones/core/image_predictive.py``. Pattern
|
| 10 |
+
identique aux autres rendus : server-side, pas de JS, anti-
|
| 11 |
+
injection systématique.
|
| 12 |
+
|
| 13 |
+
Vue
|
| 14 |
+
---
|
| 15 |
+
Deux blocs dans une section unique :
|
| 16 |
+
|
| 17 |
+
1. **Complexité paléographique** : moyenne, médiane, min, max,
|
| 18 |
+
écart-type sur l'ensemble du corpus.
|
| 19 |
+
2. **Homogénéité du corpus** : score combiné + détail par
|
| 20 |
+
feature (mean, stdev, contribution normalisée).
|
| 21 |
+
|
| 22 |
+
Adaptive : ``""`` si pas de données.
|
| 23 |
+
|
| 24 |
+
Note d'intégration
|
| 25 |
+
------------------
|
| 26 |
+
Module pur — l'utilisateur compose :
|
| 27 |
+
|
| 28 |
+
.. code-block:: python
|
| 29 |
+
|
| 30 |
+
from picarones.measurements.image_predictive import aggregate_corpus_predictive
|
| 31 |
+
from picarones.reports_v2.html.renderers.image_predictive import (
|
| 32 |
+
build_image_predictive_html,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
qualities = [doc.image_quality.as_dict() for doc in benchmark.docs]
|
| 36 |
+
agg = aggregate_corpus_predictive(qualities)
|
| 37 |
+
html = build_image_predictive_html(agg, labels)
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
from __future__ import annotations
|
| 41 |
+
|
| 42 |
+
from html import escape as _e
|
| 43 |
+
from typing import Optional
|
| 44 |
+
|
| 45 |
+
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
_FEATURE_LABEL_KEYS = {
|
| 49 |
+
"noise_level": "imgpred_feat_noise",
|
| 50 |
+
"sharpness_score": "imgpred_feat_sharpness",
|
| 51 |
+
"contrast_score": "imgpred_feat_contrast",
|
| 52 |
+
"rotation_degrees": "imgpred_feat_rotation",
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _render_complexity_block(
|
| 57 |
+
aggregated: dict, labels: dict[str, str],
|
| 58 |
+
) -> str:
|
| 59 |
+
h_complex = labels.get(
|
| 60 |
+
"imgpred_complexity", "Complexité paléographique",
|
| 61 |
+
)
|
| 62 |
+
h_mean = labels.get("imgpred_mean", "Moyenne")
|
| 63 |
+
h_median = labels.get("imgpred_median", "Médiane")
|
| 64 |
+
h_min = labels.get("imgpred_min", "Min")
|
| 65 |
+
h_max = labels.get("imgpred_max", "Max")
|
| 66 |
+
h_stdev = labels.get("imgpred_stdev", "Écart-type")
|
| 67 |
+
h_docs = labels.get("imgpred_docs", "Docs")
|
| 68 |
+
mean = float(aggregated.get("complexity_mean") or 0.0)
|
| 69 |
+
median = float(aggregated.get("complexity_median") or 0.0)
|
| 70 |
+
mn = float(aggregated.get("complexity_min") or 0.0)
|
| 71 |
+
mx = float(aggregated.get("complexity_max") or 0.0)
|
| 72 |
+
sd = float(aggregated.get("complexity_stdev") or 0.0)
|
| 73 |
+
n_docs = int(aggregated.get("n_docs") or 0)
|
| 74 |
+
color_mean = color_traffic_light(mean, low_is_good=True)
|
| 75 |
+
return (
|
| 76 |
+
f'<div style="font-weight:600;margin:.4rem 0 .3rem 0">'
|
| 77 |
+
f'{_e(h_complex)}</div>'
|
| 78 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 79 |
+
'font-size:.9rem;margin-bottom:.8rem">'
|
| 80 |
+
f'<thead><tr>'
|
| 81 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 82 |
+
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_mean)}</th>'
|
| 83 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 84 |
+
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_median)}</th>'
|
| 85 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 86 |
+
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_min)}</th>'
|
| 87 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 88 |
+
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_max)}</th>'
|
| 89 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 90 |
+
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_stdev)}</th>'
|
| 91 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:right;'
|
| 92 |
+
f'border-bottom:1px solid #ccc;font-weight:600">{_e(h_docs)}</th>'
|
| 93 |
+
f'</tr></thead>'
|
| 94 |
+
f'<tbody><tr>'
|
| 95 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 96 |
+
f'background:{color_mean};font-family:monospace;font-weight:600">'
|
| 97 |
+
f'{mean:.3f}</td>'
|
| 98 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 99 |
+
f'font-family:monospace">{median:.3f}</td>'
|
| 100 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 101 |
+
f'font-family:monospace">{mn:.3f}</td>'
|
| 102 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 103 |
+
f'font-family:monospace">{mx:.3f}</td>'
|
| 104 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 105 |
+
f'font-family:monospace">{sd:.3f}</td>'
|
| 106 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 107 |
+
f'font-family:monospace">{n_docs}</td>'
|
| 108 |
+
f'</tr></tbody></table>'
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _render_homogeneity_block(
|
| 113 |
+
homogeneity: dict, labels: dict[str, str],
|
| 114 |
+
) -> str:
|
| 115 |
+
h_homo = labels.get(
|
| 116 |
+
"imgpred_homogeneity", "Homogénéité du corpus",
|
| 117 |
+
)
|
| 118 |
+
h_feat = labels.get("imgpred_feature", "Feature")
|
| 119 |
+
h_mean = labels.get("imgpred_feat_mean", "Moyenne")
|
| 120 |
+
h_stdev = labels.get("imgpred_feat_stdev", "Écart-type")
|
| 121 |
+
h_norm = labels.get(
|
| 122 |
+
"imgpred_feat_norm", "Contribution normalisée",
|
| 123 |
+
)
|
| 124 |
+
score = float(homogeneity.get("score") or 0.0)
|
| 125 |
+
color = color_traffic_light(score, low_is_good=True)
|
| 126 |
+
parts = [
|
| 127 |
+
f'<div style="font-weight:600;margin:.4rem 0 .3rem 0">'
|
| 128 |
+
f'{_e(h_homo)} : '
|
| 129 |
+
f'<span style="background:{color};padding:.1rem .4rem;'
|
| 130 |
+
f'border-radius:.3rem;font-family:monospace">{score:.3f}</span>'
|
| 131 |
+
f'</div>',
|
| 132 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 133 |
+
'font-size:.9rem">',
|
| 134 |
+
'<thead><tr>',
|
| 135 |
+
]
|
| 136 |
+
for col in (h_feat, h_mean, h_stdev, h_norm):
|
| 137 |
+
parts.append(
|
| 138 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 139 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 140 |
+
f'{_e(col)}</th>'
|
| 141 |
+
)
|
| 142 |
+
parts.append("</tr></thead><tbody>")
|
| 143 |
+
per_feat = homogeneity.get("per_feature") or {}
|
| 144 |
+
for key, label_key in _FEATURE_LABEL_KEYS.items():
|
| 145 |
+
if key not in per_feat:
|
| 146 |
+
continue
|
| 147 |
+
slot = per_feat[key]
|
| 148 |
+
feat_label = labels.get(label_key, key)
|
| 149 |
+
feat_mean = float(slot.get("mean") or 0.0)
|
| 150 |
+
feat_stdev = float(slot.get("stdev") or 0.0)
|
| 151 |
+
feat_norm = float(slot.get("normalised") or 0.0)
|
| 152 |
+
norm_color = color_traffic_light(feat_norm, low_is_good=True)
|
| 153 |
+
parts.append(
|
| 154 |
+
f'<tr>'
|
| 155 |
+
f'<td style="padding:.4rem .6rem">{_e(feat_label)}</td>'
|
| 156 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 157 |
+
f'font-family:monospace">{feat_mean:.3f}</td>'
|
| 158 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 159 |
+
f'font-family:monospace">{feat_stdev:.3f}</td>'
|
| 160 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 161 |
+
f'background:{norm_color};font-family:monospace">'
|
| 162 |
+
f'{feat_norm:.3f}</td>'
|
| 163 |
+
f'</tr>'
|
| 164 |
+
)
|
| 165 |
+
parts.append("</tbody></table>")
|
| 166 |
+
return "".join(parts)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def build_image_predictive_html(
|
| 170 |
+
aggregated: Optional[dict],
|
| 171 |
+
labels: Optional[dict[str, str]] = None,
|
| 172 |
+
) -> str:
|
| 173 |
+
"""Construit la vue HTML « Profil d'image du corpus ».
|
| 174 |
+
|
| 175 |
+
Parameters
|
| 176 |
+
----------
|
| 177 |
+
aggregated:
|
| 178 |
+
Sortie de ``aggregate_corpus_predictive``. Si ``None``
|
| 179 |
+
ou ``n_docs == 0``, retourne ``""``.
|
| 180 |
+
labels:
|
| 181 |
+
Dict i18n. Clés sous le préfixe ``imgpred_*``.
|
| 182 |
+
"""
|
| 183 |
+
if not aggregated:
|
| 184 |
+
return ""
|
| 185 |
+
if not aggregated.get("n_docs"):
|
| 186 |
+
return ""
|
| 187 |
+
labels = labels or {}
|
| 188 |
+
title = labels.get(
|
| 189 |
+
"imgpred_title", "Profil d'image du corpus",
|
| 190 |
+
)
|
| 191 |
+
note = labels.get(
|
| 192 |
+
"imgpred_note",
|
| 193 |
+
"Score de complexité paléographique combinant bruit, "
|
| 194 |
+
"flou, faible contraste et rotation. Le score "
|
| 195 |
+
"d'homogénéité signale si la moyenne globale est fiable "
|
| 196 |
+
"(corpus uniforme) ou trompeuse (corpus hétérogène — "
|
| 197 |
+
"voir alors la vue stratifiée).",
|
| 198 |
+
)
|
| 199 |
+
parts = [
|
| 200 |
+
'<section class="imgpred-section" style="margin:1rem 0">',
|
| 201 |
+
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 202 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 203 |
+
f'{_e(note)}</div>',
|
| 204 |
+
]
|
| 205 |
+
parts.append(_render_complexity_block(aggregated, labels))
|
| 206 |
+
homo = aggregated.get("homogeneity")
|
| 207 |
+
if isinstance(homo, dict):
|
| 208 |
+
parts.append(_render_homogeneity_block(homo, labels))
|
| 209 |
+
parts.append("</section>")
|
| 210 |
+
return "".join(parts)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
__all__ = ["build_image_predictive_html"]
|
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML « Comparaison contrôlée » — Sprint 96 (B.5).
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.incremental_comparison_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.incremental_comparison``.
|
| 6 |
+
Le chemin legacy reste disponible via un shim avec
|
| 7 |
+
``DeprecationWarning`` ; suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Suite directe ``picarones/core/incremental_comparison.py``.
|
| 10 |
+
Pattern identique aux autres rendus : server-side, pas de JS,
|
| 11 |
+
anti-injection systématique.
|
| 12 |
+
|
| 13 |
+
Vue
|
| 14 |
+
---
|
| 15 |
+
Tableau ANOVA-like : pour chaque valeur du slot variant, mean
|
| 16 |
+
± stdev, rang moyen, n_observations. Mean colorée en
|
| 17 |
+
gradient vert (meilleur) → rouge (pire), normalisée sur la
|
| 18 |
+
plage des moyennes observées.
|
| 19 |
+
|
| 20 |
+
Adaptive : ``""`` si ``analysis`` est ``None``.
|
| 21 |
+
|
| 22 |
+
Note d'intégration
|
| 23 |
+
------------------
|
| 24 |
+
Module pur — l'utilisateur compose :
|
| 25 |
+
|
| 26 |
+
.. code-block:: python
|
| 27 |
+
|
| 28 |
+
from picarones.measurements.incremental_comparison import (
|
| 29 |
+
PipelineRun, compare_isolated_effect,
|
| 30 |
+
)
|
| 31 |
+
from picarones.reports_v2.html.renderers.incremental_comparison import (
|
| 32 |
+
build_incremental_comparison_html,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
runs = [
|
| 36 |
+
PipelineRun(name=p.name,
|
| 37 |
+
slots={"ocr": p.ocr, "llm": p.llm},
|
| 38 |
+
score=p.cer_mean)
|
| 39 |
+
for p in benchmark.pipelines
|
| 40 |
+
]
|
| 41 |
+
analysis = compare_isolated_effect(runs, "llm")
|
| 42 |
+
html = build_incremental_comparison_html(analysis, labels)
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
from __future__ import annotations
|
| 46 |
+
|
| 47 |
+
from html import escape as _e
|
| 48 |
+
from typing import Optional
|
| 49 |
+
|
| 50 |
+
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _bg_for_relative_score(
|
| 54 |
+
score: float, low: float, high: float, higher_is_better: bool,
|
| 55 |
+
) -> str:
|
| 56 |
+
"""Mappe ``score`` sur une plage [low, high] et retourne une cellule
|
| 57 |
+
colorée traffic-light.
|
| 58 |
+
|
| 59 |
+
Si ``higher_is_better=True``, ``score=high`` est vert ; sinon
|
| 60 |
+
``score=low`` est vert.
|
| 61 |
+
"""
|
| 62 |
+
if high == low:
|
| 63 |
+
return color_traffic_light(1.0) # neutre vert clair
|
| 64 |
+
return color_traffic_light(
|
| 65 |
+
score,
|
| 66 |
+
low_is_good=not higher_is_better,
|
| 67 |
+
scale_min=low,
|
| 68 |
+
scale_max=high,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _format_score(value: Optional[float]) -> str:
|
| 73 |
+
if value is None:
|
| 74 |
+
return "—"
|
| 75 |
+
if abs(value) < 1.0:
|
| 76 |
+
return f"{value * 100:.2f}%"
|
| 77 |
+
return f"{value:.3f}"
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def build_incremental_comparison_html(
|
| 81 |
+
analysis: Optional[dict],
|
| 82 |
+
labels: Optional[dict[str, str]] = None,
|
| 83 |
+
) -> str:
|
| 84 |
+
"""Construit la vue HTML « Comparaison contrôlée ».
|
| 85 |
+
|
| 86 |
+
Parameters
|
| 87 |
+
----------
|
| 88 |
+
analysis:
|
| 89 |
+
Sortie de ``compare_isolated_effect``. ``None`` ou
|
| 90 |
+
``per_value`` vide → retourne ``""``.
|
| 91 |
+
labels:
|
| 92 |
+
Dict i18n. Clés sous le préfixe ``incr_*``.
|
| 93 |
+
"""
|
| 94 |
+
if not analysis:
|
| 95 |
+
return ""
|
| 96 |
+
per_value = analysis.get("per_value") or {}
|
| 97 |
+
if not per_value:
|
| 98 |
+
return ""
|
| 99 |
+
labels = labels or {}
|
| 100 |
+
title = labels.get(
|
| 101 |
+
"incr_title", "Comparaison contrôlée par slot",
|
| 102 |
+
)
|
| 103 |
+
note = labels.get(
|
| 104 |
+
"incr_note",
|
| 105 |
+
"Effet isolé du module variant sur les pipelines en "
|
| 106 |
+
"contrôlant les autres slots. Pour chaque valeur du "
|
| 107 |
+
"slot, moyenne ± écart-type, rang moyen sur les groupes "
|
| 108 |
+
"fixes, et nombre d'observations. Type design "
|
| 109 |
+
"d'expérience pour des comparaisons honnêtes.",
|
| 110 |
+
)
|
| 111 |
+
slot_label = labels.get("incr_slot_label", "Slot variant")
|
| 112 |
+
h_value = labels.get("incr_value", "Valeur")
|
| 113 |
+
h_mean = labels.get("incr_mean", "Score moyen")
|
| 114 |
+
h_stdev = labels.get("incr_stdev", "± σ")
|
| 115 |
+
h_rank = labels.get("incr_rank", "Rang moyen")
|
| 116 |
+
h_n_obs = labels.get("incr_n_obs", "Observations")
|
| 117 |
+
h_groups = labels.get("incr_groups", "Groupes fixes")
|
| 118 |
+
higher_is_better = bool(analysis.get("higher_is_better", False))
|
| 119 |
+
|
| 120 |
+
# Plage de moyennes pour le code couleur
|
| 121 |
+
means = [
|
| 122 |
+
d["mean"] for d in per_value.values() if d.get("mean") is not None
|
| 123 |
+
]
|
| 124 |
+
low = min(means) if means else 0.0
|
| 125 |
+
high = max(means) if means else 0.0
|
| 126 |
+
|
| 127 |
+
varying_slot = str(analysis.get("varying_slot") or "?")
|
| 128 |
+
n_groups = int(analysis.get("n_groups") or 0)
|
| 129 |
+
n_runs = int(analysis.get("n_runs") or 0)
|
| 130 |
+
best = analysis.get("best_value")
|
| 131 |
+
worst = analysis.get("worst_value")
|
| 132 |
+
|
| 133 |
+
parts = [
|
| 134 |
+
'<section class="incr-section" style="margin:1rem 0">',
|
| 135 |
+
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 136 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 137 |
+
f'{_e(note)}</div>',
|
| 138 |
+
f'<div style="font-size:.85rem;margin-bottom:.5rem">'
|
| 139 |
+
f'<strong>{_e(slot_label)} :</strong> '
|
| 140 |
+
f'<code>{_e(varying_slot)}</code> '
|
| 141 |
+
f'<span style="opacity:.75">'
|
| 142 |
+
f'{n_runs} runs, {n_groups} {_e(h_groups.lower())}'
|
| 143 |
+
f'</span></div>',
|
| 144 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 145 |
+
'font-size:.9rem">',
|
| 146 |
+
'<thead><tr>',
|
| 147 |
+
]
|
| 148 |
+
for col in (h_value, h_mean, h_stdev, h_rank, h_n_obs):
|
| 149 |
+
parts.append(
|
| 150 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 151 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 152 |
+
f'{_e(col)}</th>'
|
| 153 |
+
)
|
| 154 |
+
parts.append("</tr></thead><tbody>")
|
| 155 |
+
|
| 156 |
+
# Tri par rang moyen ascendant
|
| 157 |
+
rows = sorted(
|
| 158 |
+
per_value.items(),
|
| 159 |
+
key=lambda kv: (kv[1].get("mean_rank") or float("inf")),
|
| 160 |
+
)
|
| 161 |
+
for value, d in rows:
|
| 162 |
+
mean = d.get("mean")
|
| 163 |
+
stdev = d.get("stdev")
|
| 164 |
+
rank = d.get("mean_rank")
|
| 165 |
+
n_obs = int(d.get("n_observations") or 0)
|
| 166 |
+
if isinstance(mean, (int, float)):
|
| 167 |
+
color = _bg_for_relative_score(
|
| 168 |
+
float(mean), low, high, higher_is_better,
|
| 169 |
+
)
|
| 170 |
+
mean_cell = (
|
| 171 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 172 |
+
f'background:{color};font-family:monospace;'
|
| 173 |
+
f'font-weight:600">{_format_score(mean)}</td>'
|
| 174 |
+
)
|
| 175 |
+
else:
|
| 176 |
+
mean_cell = (
|
| 177 |
+
'<td style="padding:.4rem .6rem;text-align:right;'
|
| 178 |
+
'opacity:.4">—</td>'
|
| 179 |
+
)
|
| 180 |
+
stdev_str = (
|
| 181 |
+
f"± {_format_score(stdev)}"
|
| 182 |
+
if isinstance(stdev, (int, float)) else "—"
|
| 183 |
+
)
|
| 184 |
+
rank_str = f"{rank:.2f}" if isinstance(rank, (int, float)) else "—"
|
| 185 |
+
marker = ""
|
| 186 |
+
if value == best:
|
| 187 |
+
marker = ' <span style="color:#16a34a">★</span>'
|
| 188 |
+
elif value == worst:
|
| 189 |
+
marker = ' <span style="color:#dc2626">▼</span>'
|
| 190 |
+
parts.append(
|
| 191 |
+
f'<tr>'
|
| 192 |
+
f'<td style="padding:.4rem .6rem;font-family:monospace">'
|
| 193 |
+
f'{_e(str(value))}{marker}</td>'
|
| 194 |
+
f'{mean_cell}'
|
| 195 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 196 |
+
f'font-family:monospace">{stdev_str}</td>'
|
| 197 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 198 |
+
f'font-family:monospace">{rank_str}</td>'
|
| 199 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 200 |
+
f'font-family:monospace">{n_obs}</td>'
|
| 201 |
+
f'</tr>'
|
| 202 |
+
)
|
| 203 |
+
parts.append("</tbody></table></section>")
|
| 204 |
+
return "".join(parts)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
__all__ = ["build_incremental_comparison_html"]
|
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML « Modules audités » — Sprint 97 (B.6).
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.module_audit_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.module_audit``. Le chemin
|
| 6 |
+
legacy reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 7 |
+
suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Suite directe ``picarones/core/module_policy.py``. Pattern
|
| 10 |
+
identique aux autres rendus : server-side, pas de JS, anti-
|
| 11 |
+
injection systématique.
|
| 12 |
+
|
| 13 |
+
Vue
|
| 14 |
+
---
|
| 15 |
+
Tableau récapitulatif des modules utilisés dans une pipeline
|
| 16 |
+
composée, chacun avec :
|
| 17 |
+
|
| 18 |
+
- Statut d'audit (✓ vert si tous les checks passent, ✗ rouge
|
| 19 |
+
sinon, avec compte des échecs) ;
|
| 20 |
+
- Métadonnées : version, auteur, licence ;
|
| 21 |
+
- Citation académique si fournie ;
|
| 22 |
+
- Lien vers la homepage si fourni.
|
| 23 |
+
|
| 24 |
+
Adaptive : ``""`` si la liste est vide.
|
| 25 |
+
|
| 26 |
+
Note d'intégration
|
| 27 |
+
------------------
|
| 28 |
+
Module pur — l'utilisateur compose la liste depuis sa
|
| 29 |
+
``PipelineSpec`` augmentée des ``ModuleManifest`` :
|
| 30 |
+
|
| 31 |
+
.. code-block:: python
|
| 32 |
+
|
| 33 |
+
from picarones.measurements.module_policy import audit_module
|
| 34 |
+
from picarones.reports_v2.html.renderers.module_audit import build_module_audit_html
|
| 35 |
+
|
| 36 |
+
audits = []
|
| 37 |
+
for step in pipeline.steps:
|
| 38 |
+
manifest = step.module.manifest # convention applicative
|
| 39 |
+
result = audit_module(step.module, manifest)
|
| 40 |
+
audits.append({
|
| 41 |
+
"manifest": manifest.as_dict(),
|
| 42 |
+
"audit": result.as_dict(),
|
| 43 |
+
})
|
| 44 |
+
html = build_module_audit_html(audits, labels)
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
from __future__ import annotations
|
| 48 |
+
|
| 49 |
+
from html import escape as _e
|
| 50 |
+
from typing import Optional
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _passed_badge(passed: bool, n_failed: int, label_pass: str,
|
| 54 |
+
label_fail: str) -> str:
|
| 55 |
+
if passed:
|
| 56 |
+
return (
|
| 57 |
+
f'<span style="color:#16a34a;font-weight:700">'
|
| 58 |
+
f'✓ {_e(label_pass)}</span>'
|
| 59 |
+
)
|
| 60 |
+
return (
|
| 61 |
+
f'<span style="color:#dc2626;font-weight:700">'
|
| 62 |
+
f'✗ {_e(label_fail)} ({n_failed})</span>'
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def build_module_audit_html(
|
| 67 |
+
audits: Optional[list],
|
| 68 |
+
labels: Optional[dict[str, str]] = None,
|
| 69 |
+
) -> str:
|
| 70 |
+
"""Construit la vue HTML « Modules audités ».
|
| 71 |
+
|
| 72 |
+
Parameters
|
| 73 |
+
----------
|
| 74 |
+
audits:
|
| 75 |
+
Liste de dicts ``{"manifest": ManifestDict, "audit":
|
| 76 |
+
AuditResultDict}``. Si vide ou ``None``, retourne ``""``.
|
| 77 |
+
labels:
|
| 78 |
+
Dict i18n. Clés sous le préfixe ``audit_*``.
|
| 79 |
+
"""
|
| 80 |
+
if not audits:
|
| 81 |
+
return ""
|
| 82 |
+
rows = [
|
| 83 |
+
a for a in audits
|
| 84 |
+
if isinstance(a, dict)
|
| 85 |
+
and isinstance(a.get("manifest"), dict)
|
| 86 |
+
and isinstance(a.get("audit"), dict)
|
| 87 |
+
]
|
| 88 |
+
if not rows:
|
| 89 |
+
return ""
|
| 90 |
+
labels = labels or {}
|
| 91 |
+
title = labels.get("audit_title", "Modules audités")
|
| 92 |
+
note = labels.get(
|
| 93 |
+
"audit_note",
|
| 94 |
+
"Récapitulatif des modules utilisés dans la pipeline "
|
| 95 |
+
"composée. Un module qui ne passe pas l'audit n'est "
|
| 96 |
+
"pas exécutable. Métadonnées issues du manifest fourni "
|
| 97 |
+
"par le contributeur (auteur, licence, citation).",
|
| 98 |
+
)
|
| 99 |
+
label_pass = labels.get("audit_pass", "audit OK")
|
| 100 |
+
label_fail = labels.get("audit_fail", "checks échoués")
|
| 101 |
+
h_module = labels.get("audit_module", "Module")
|
| 102 |
+
h_status = labels.get("audit_status", "Audit")
|
| 103 |
+
h_version = labels.get("audit_version", "Version")
|
| 104 |
+
h_author = labels.get("audit_author", "Auteur")
|
| 105 |
+
h_license = labels.get("audit_license", "Licence")
|
| 106 |
+
h_io = labels.get("audit_io", "Entrée → sortie")
|
| 107 |
+
h_citation = labels.get("audit_citation", "Citation")
|
| 108 |
+
h_homepage = labels.get("audit_homepage", "Page projet")
|
| 109 |
+
|
| 110 |
+
parts = [
|
| 111 |
+
'<section class="audit-section" style="margin:1rem 0">',
|
| 112 |
+
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 113 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 114 |
+
f'{_e(note)}</div>',
|
| 115 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 116 |
+
'font-size:.9rem">',
|
| 117 |
+
'<thead><tr>',
|
| 118 |
+
]
|
| 119 |
+
for col in (h_module, h_status, h_version, h_author,
|
| 120 |
+
h_license, h_io, h_citation, h_homepage):
|
| 121 |
+
parts.append(
|
| 122 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 123 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 124 |
+
f'{_e(col)}</th>'
|
| 125 |
+
)
|
| 126 |
+
parts.append("</tr></thead><tbody>")
|
| 127 |
+
|
| 128 |
+
for entry in rows:
|
| 129 |
+
manifest = entry["manifest"]
|
| 130 |
+
audit = entry["audit"]
|
| 131 |
+
name = str(manifest.get("name") or "?")
|
| 132 |
+
version = str(manifest.get("version") or "—")
|
| 133 |
+
author = str(manifest.get("author") or "—")
|
| 134 |
+
license_ = str(manifest.get("license") or "—")
|
| 135 |
+
in_types = ", ".join(manifest.get("input_types") or []) or "—"
|
| 136 |
+
out_types = ", ".join(manifest.get("output_types") or []) or "—"
|
| 137 |
+
citation = manifest.get("citation") or ""
|
| 138 |
+
homepage = manifest.get("homepage") or ""
|
| 139 |
+
passed = bool(audit.get("passed"))
|
| 140 |
+
n_failed = int(audit.get("n_failed") or 0)
|
| 141 |
+
status_cell = _passed_badge(
|
| 142 |
+
passed, n_failed, label_pass, label_fail,
|
| 143 |
+
)
|
| 144 |
+
# Citation : tronqué si trop long
|
| 145 |
+
citation_str = str(citation)[:120]
|
| 146 |
+
if len(str(citation)) > 120:
|
| 147 |
+
citation_str += "…"
|
| 148 |
+
citation_cell = (
|
| 149 |
+
_e(citation_str) if citation_str.strip() else "—"
|
| 150 |
+
)
|
| 151 |
+
# Homepage : on n'auto-link **pas** (anti-injection +
|
| 152 |
+
# honnêteté : l'URL peut pointer ailleurs). On affiche
|
| 153 |
+
# le texte échappé tel quel.
|
| 154 |
+
homepage_cell = (
|
| 155 |
+
_e(str(homepage))[:80] + ("…" if len(str(homepage)) > 80 else "")
|
| 156 |
+
) if str(homepage).strip() else "—"
|
| 157 |
+
parts.append(
|
| 158 |
+
f'<tr>'
|
| 159 |
+
f'<td style="padding:.4rem .6rem;font-family:monospace">'
|
| 160 |
+
f'{_e(name)}</td>'
|
| 161 |
+
f'<td style="padding:.4rem .6rem">{status_cell}</td>'
|
| 162 |
+
f'<td style="padding:.4rem .6rem;font-family:monospace">'
|
| 163 |
+
f'{_e(version)}</td>'
|
| 164 |
+
f'<td style="padding:.4rem .6rem">{_e(author)}</td>'
|
| 165 |
+
f'<td style="padding:.4rem .6rem;font-family:monospace">'
|
| 166 |
+
f'{_e(license_)}</td>'
|
| 167 |
+
f'<td style="padding:.4rem .6rem;font-family:monospace;'
|
| 168 |
+
f'font-size:.8rem">{_e(in_types)} → {_e(out_types)}</td>'
|
| 169 |
+
f'<td style="padding:.4rem .6rem;font-size:.8rem;'
|
| 170 |
+
f'opacity:.85">{citation_cell}</td>'
|
| 171 |
+
f'<td style="padding:.4rem .6rem;font-family:monospace;'
|
| 172 |
+
f'font-size:.8rem">{homepage_cell}</td>'
|
| 173 |
+
f'</tr>'
|
| 174 |
+
)
|
| 175 |
+
parts.append("</tbody></table></section>")
|
| 176 |
+
return "".join(parts)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
__all__ = ["build_module_audit_html"]
|
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML server-side de la section NER (Sprint 41).
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis ``picarones.report.ner_render``
|
| 4 |
+
vers ``picarones.reports_v2.html.renderers.ner``. Le chemin legacy
|
| 5 |
+
reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 6 |
+
suppression prévue en 2.0.
|
| 7 |
+
|
| 8 |
+
Suite directe des Sprints 38-40 : la couche de calcul, le backend
|
| 9 |
+
extracteur et le câblage runner sont en place ; ce module produit les
|
| 10 |
+
blocs HTML qui remontent ces données dans le rapport.
|
| 11 |
+
|
| 12 |
+
- ``build_ner_summary_html`` — encart factuel par moteur : F1 global,
|
| 13 |
+
precision/recall, total entités, hallucinations, missed.
|
| 14 |
+
- ``build_ner_per_category_html`` — table heatmap moteur × catégorie,
|
| 15 |
+
cellules colorées par F1 (rouge → vert).
|
| 16 |
+
|
| 17 |
+
Principe — cohérent avec ``inter_engine_render`` (Sprint 37) : rendu
|
| 18 |
+
server-side, pas de JavaScript, déterministe. Si aucun moteur n'a de
|
| 19 |
+
``aggregated_ner``, les fonctions retournent une chaîne vide — la vue
|
| 20 |
+
est silencieusement omise (rapport adaptatif).
|
| 21 |
+
|
| 22 |
+
Anti-injection : tous les noms de moteurs et catégories sont passés à
|
| 23 |
+
``html.escape`` avant insertion.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
from html import escape as _e
|
| 29 |
+
from typing import Optional
|
| 30 |
+
|
| 31 |
+
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _engines_with_ner(engines_summary: list[dict]) -> list[dict]:
|
| 35 |
+
"""Filtre les moteurs qui ont une analyse NER agrégée."""
|
| 36 |
+
return [e for e in engines_summary if e.get("aggregated_ner")]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def build_ner_summary_html(
|
| 40 |
+
engines_summary: list[dict],
|
| 41 |
+
labels: Optional[dict[str, str]] = None,
|
| 42 |
+
) -> str:
|
| 43 |
+
"""Construit l'encart résumé NER : F1 global par moteur + totaux.
|
| 44 |
+
|
| 45 |
+
Parameters
|
| 46 |
+
----------
|
| 47 |
+
engines_summary:
|
| 48 |
+
Liste de dicts moteur (au moins ``name`` et ``aggregated_ner``).
|
| 49 |
+
labels:
|
| 50 |
+
Dict d'étiquettes i18n.
|
| 51 |
+
|
| 52 |
+
Returns
|
| 53 |
+
-------
|
| 54 |
+
str
|
| 55 |
+
HTML ``<div>...</div>`` ou ``""`` si aucun moteur n'a de NER.
|
| 56 |
+
"""
|
| 57 |
+
relevant = _engines_with_ner(engines_summary)
|
| 58 |
+
if not relevant:
|
| 59 |
+
return ""
|
| 60 |
+
|
| 61 |
+
labels = labels or {}
|
| 62 |
+
caption = labels.get("ner_summary_caption", "Précision sur entités nommées")
|
| 63 |
+
engine_label = labels.get("ner_engine_label", "Moteur")
|
| 64 |
+
f1_label = labels.get("ner_f1_label", "F1 global")
|
| 65 |
+
p_label = labels.get("ner_precision_label", "Précision")
|
| 66 |
+
r_label = labels.get("ner_recall_label", "Rappel")
|
| 67 |
+
docs_label = labels.get("ner_doc_count_label", "Docs évalués")
|
| 68 |
+
halluc_label = labels.get("ner_hallucinated_label", "Hallucinations")
|
| 69 |
+
missed_label = labels.get("ner_missed_label", "Entités manquées")
|
| 70 |
+
|
| 71 |
+
parts: list[str] = []
|
| 72 |
+
parts.append('<div class="ner-summary">')
|
| 73 |
+
parts.append(
|
| 74 |
+
f'<div class="ner-summary-caption" style="font-weight:600;'
|
| 75 |
+
f'margin-bottom:.4rem">{_e(caption)}</div>'
|
| 76 |
+
)
|
| 77 |
+
parts.append(
|
| 78 |
+
'<table class="ner-summary-table" '
|
| 79 |
+
'style="border-collapse:collapse;font-size:.85rem;width:100%">'
|
| 80 |
+
)
|
| 81 |
+
parts.append("<thead><tr>")
|
| 82 |
+
for hdr in (engine_label, f1_label, p_label, r_label,
|
| 83 |
+
docs_label, halluc_label, missed_label):
|
| 84 |
+
parts.append(
|
| 85 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 86 |
+
f'border-bottom:1px solid var(--border);font-weight:600">'
|
| 87 |
+
f'{_e(hdr)}</th>'
|
| 88 |
+
)
|
| 89 |
+
parts.append("</tr></thead><tbody>")
|
| 90 |
+
for engine in relevant:
|
| 91 |
+
agg = engine["aggregated_ner"]
|
| 92 |
+
global_stats = agg.get("global", {}) or {}
|
| 93 |
+
f1 = float(global_stats.get("f1") or 0.0)
|
| 94 |
+
precision = float(global_stats.get("precision") or 0.0)
|
| 95 |
+
recall = float(global_stats.get("recall") or 0.0)
|
| 96 |
+
doc_count = int(agg.get("doc_count") or 0)
|
| 97 |
+
hallucinated = int(agg.get("hallucinated_total") or 0)
|
| 98 |
+
missed = int(agg.get("missed_total") or 0)
|
| 99 |
+
bg = color_traffic_light(f1)
|
| 100 |
+
parts.append("<tr>")
|
| 101 |
+
parts.append(
|
| 102 |
+
f'<td style="padding:.3rem .5rem;font-weight:600">'
|
| 103 |
+
f'{_e(engine.get("name", ""))}</td>'
|
| 104 |
+
)
|
| 105 |
+
parts.append(
|
| 106 |
+
f'<td style="padding:.3rem .5rem;background:{bg};'
|
| 107 |
+
f'font-variant-numeric:tabular-nums">{f1 * 100:.1f} %</td>'
|
| 108 |
+
)
|
| 109 |
+
parts.append(
|
| 110 |
+
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 111 |
+
f'{precision * 100:.1f} %</td>'
|
| 112 |
+
)
|
| 113 |
+
parts.append(
|
| 114 |
+
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 115 |
+
f'{recall * 100:.1f} %</td>'
|
| 116 |
+
)
|
| 117 |
+
parts.append(
|
| 118 |
+
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 119 |
+
f'{doc_count}</td>'
|
| 120 |
+
)
|
| 121 |
+
parts.append(
|
| 122 |
+
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 123 |
+
f'{hallucinated}</td>'
|
| 124 |
+
)
|
| 125 |
+
parts.append(
|
| 126 |
+
f'<td style="padding:.3rem .5rem;font-variant-numeric:tabular-nums">'
|
| 127 |
+
f'{missed}</td>'
|
| 128 |
+
)
|
| 129 |
+
parts.append("</tr>")
|
| 130 |
+
parts.append("</tbody></table></div>")
|
| 131 |
+
return "".join(parts)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def build_ner_per_category_html(
|
| 135 |
+
engines_summary: list[dict],
|
| 136 |
+
labels: Optional[dict[str, str]] = None,
|
| 137 |
+
) -> str:
|
| 138 |
+
"""Construit la heatmap NER moteur × catégorie d'entité.
|
| 139 |
+
|
| 140 |
+
Lignes = moteurs, colonnes = catégories (PER, LOC, ORG, DATE,
|
| 141 |
+
MISC…). Cellules colorées par F1 (rouge → vert). La cellule
|
| 142 |
+
affiche le F1 en pourcentage. Cellules vides quand la catégorie
|
| 143 |
+
n'a pas été observée pour le moteur.
|
| 144 |
+
|
| 145 |
+
Returns
|
| 146 |
+
-------
|
| 147 |
+
str
|
| 148 |
+
HTML ``<div>...</div>`` ou ``""`` si pas de données.
|
| 149 |
+
"""
|
| 150 |
+
relevant = _engines_with_ner(engines_summary)
|
| 151 |
+
if not relevant:
|
| 152 |
+
return ""
|
| 153 |
+
|
| 154 |
+
# Catégories : union sur tous les moteurs, ordre alphabétique
|
| 155 |
+
all_categories: set[str] = set()
|
| 156 |
+
for engine in relevant:
|
| 157 |
+
per_cat = (engine["aggregated_ner"] or {}).get("per_category") or {}
|
| 158 |
+
all_categories.update(per_cat.keys())
|
| 159 |
+
if not all_categories:
|
| 160 |
+
return ""
|
| 161 |
+
categories = sorted(all_categories)
|
| 162 |
+
|
| 163 |
+
labels = labels or {}
|
| 164 |
+
caption = labels.get(
|
| 165 |
+
"ner_per_category_caption",
|
| 166 |
+
"F1 par catégorie d'entité (heatmap)",
|
| 167 |
+
)
|
| 168 |
+
engine_label = labels.get("ner_engine_label", "Moteur")
|
| 169 |
+
no_data = labels.get("ner_no_data_label", "—")
|
| 170 |
+
|
| 171 |
+
parts: list[str] = []
|
| 172 |
+
parts.append('<div class="ner-per-category">')
|
| 173 |
+
parts.append(
|
| 174 |
+
f'<div class="ner-per-category-caption" '
|
| 175 |
+
f'style="font-weight:600;margin-bottom:.4rem">{_e(caption)}</div>'
|
| 176 |
+
)
|
| 177 |
+
parts.append(
|
| 178 |
+
'<table class="ner-per-category-table" '
|
| 179 |
+
'style="border-collapse:collapse;font-size:.8rem">'
|
| 180 |
+
)
|
| 181 |
+
parts.append("<thead><tr>")
|
| 182 |
+
parts.append(
|
| 183 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 184 |
+
f'border-bottom:1px solid var(--border)">{_e(engine_label)}</th>'
|
| 185 |
+
)
|
| 186 |
+
for cat in categories:
|
| 187 |
+
parts.append(
|
| 188 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:center;'
|
| 189 |
+
f'border-bottom:1px solid var(--border)">{_e(cat)}</th>'
|
| 190 |
+
)
|
| 191 |
+
parts.append("</tr></thead><tbody>")
|
| 192 |
+
for engine in relevant:
|
| 193 |
+
per_cat = (engine["aggregated_ner"] or {}).get("per_category") or {}
|
| 194 |
+
parts.append("<tr>")
|
| 195 |
+
parts.append(
|
| 196 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:right;'
|
| 197 |
+
f'border-right:1px solid var(--border);font-weight:600">'
|
| 198 |
+
f'{_e(engine.get("name", ""))}</th>'
|
| 199 |
+
)
|
| 200 |
+
for cat in categories:
|
| 201 |
+
stats = per_cat.get(cat)
|
| 202 |
+
if not stats or int(stats.get("support", 0)) == 0:
|
| 203 |
+
parts.append(
|
| 204 |
+
f'<td style="padding:.3rem .5rem;text-align:center;'
|
| 205 |
+
f'background:#f4f4f4;color:var(--text-muted);'
|
| 206 |
+
f'font-style:italic">{_e(no_data)}</td>'
|
| 207 |
+
)
|
| 208 |
+
else:
|
| 209 |
+
f1 = float(stats.get("f1") or 0.0)
|
| 210 |
+
support = int(stats.get("support", 0))
|
| 211 |
+
bg = color_traffic_light(f1)
|
| 212 |
+
parts.append(
|
| 213 |
+
f'<td style="padding:.3rem .5rem;text-align:center;'
|
| 214 |
+
f'background:{bg};color:#222;'
|
| 215 |
+
f'font-variant-numeric:tabular-nums" '
|
| 216 |
+
f'title="support={support}">'
|
| 217 |
+
f'{f1 * 100:.1f} %</td>'
|
| 218 |
+
)
|
| 219 |
+
parts.append("</tr>")
|
| 220 |
+
parts.append("</tbody></table></div>")
|
| 221 |
+
return "".join(parts)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
__all__ = [
|
| 225 |
+
"build_ner_summary_html",
|
| 226 |
+
"build_ner_per_category_html",
|
| 227 |
+
]
|
|
@@ -29,7 +29,7 @@ from picarones.measurements.error_absorption import (
|
|
| 29 |
aggregate_error_absorption,
|
| 30 |
compute_error_absorption,
|
| 31 |
)
|
| 32 |
-
from picarones.
|
| 33 |
build_error_absorption_html,
|
| 34 |
)
|
| 35 |
|
|
|
|
| 29 |
aggregate_error_absorption,
|
| 30 |
compute_error_absorption,
|
| 31 |
)
|
| 32 |
+
from picarones.reports_v2.html.renderers.error_absorption import (
|
| 33 |
build_error_absorption_html,
|
| 34 |
)
|
| 35 |
|
|
@@ -35,7 +35,7 @@ from picarones.measurements.image_predictive import (
|
|
| 35 |
compute_corpus_homogeneity,
|
| 36 |
compute_paleographic_complexity,
|
| 37 |
)
|
| 38 |
-
from picarones.
|
| 39 |
build_image_predictive_html,
|
| 40 |
)
|
| 41 |
|
|
|
|
| 35 |
compute_corpus_homogeneity,
|
| 36 |
compute_paleographic_complexity,
|
| 37 |
)
|
| 38 |
+
from picarones.reports_v2.html.renderers.image_predictive import (
|
| 39 |
build_image_predictive_html,
|
| 40 |
)
|
| 41 |
|
|
@@ -31,7 +31,7 @@ from picarones.measurements.incremental_comparison import (
|
|
| 31 |
PipelineRun,
|
| 32 |
compare_isolated_effect,
|
| 33 |
)
|
| 34 |
-
from picarones.
|
| 35 |
build_incremental_comparison_html,
|
| 36 |
)
|
| 37 |
|
|
|
|
| 31 |
PipelineRun,
|
| 32 |
compare_isolated_effect,
|
| 33 |
)
|
| 34 |
+
from picarones.reports_v2.html.renderers.incremental_comparison import (
|
| 35 |
build_incremental_comparison_html,
|
| 36 |
)
|
| 37 |
|
|
@@ -36,7 +36,7 @@ from picarones.measurements.module_policy import (
|
|
| 36 |
validate_manifest,
|
| 37 |
)
|
| 38 |
from picarones.core.modules import ArtifactType, BaseModule
|
| 39 |
-
from picarones.
|
| 40 |
build_module_audit_html,
|
| 41 |
)
|
| 42 |
|
|
|
|
| 36 |
validate_manifest,
|
| 37 |
)
|
| 38 |
from picarones.core.modules import ArtifactType, BaseModule
|
| 39 |
+
from picarones.reports_v2.html.renderers.module_audit import (
|
| 40 |
build_module_audit_html,
|
| 41 |
)
|
| 42 |
|
|
@@ -23,7 +23,7 @@ import pytest
|
|
| 23 |
|
| 24 |
from picarones.fixtures import generate_sample_benchmark
|
| 25 |
from picarones.report.generator import ReportGenerator
|
| 26 |
-
from picarones.
|
| 27 |
build_ner_per_category_html,
|
| 28 |
build_ner_summary_html,
|
| 29 |
)
|
|
|
|
| 23 |
|
| 24 |
from picarones.fixtures import generate_sample_benchmark
|
| 25 |
from picarones.report.generator import ReportGenerator
|
| 26 |
+
from picarones.reports_v2.html.renderers.ner import (
|
| 27 |
build_ner_per_category_html,
|
| 28 |
build_ner_summary_html,
|
| 29 |
)
|