Spaces:
Sleeping
feat(migration): Phase 5.C batch 5 — 5 renderers moyens-gros vers reports_v2/html/
Browse filesCinquième vague. Inclut les 3 renderers de la famille
``taxonomy``, ``worst_lines`` et ``pipeline_dag``.
Migrations effectuées
---------------------
| Source legacy | Destination canonique |
|-------------------------------------------------|------------------------------------------------------|
| ``report/taxonomy_intra_doc_render.py`` (148) | ``reports_v2/html/renderers/taxonomy_intra_doc.py`` |
| ``report/taxonomy_cooccurrence_render.py`` (161)| ``reports_v2/html/renderers/taxonomy_cooccurrence.py``|
| ``report/worst_lines_render.py`` (164) | ``reports_v2/html/renderers/worst_lines.py`` |
| ``report/taxonomy_comparison_render.py`` (233) | ``reports_v2/html/renderers/taxonomy_comparison.py`` |
| ``report/pipeline_dag_render.py`` (314) | ``reports_v2/html/renderers/pipeline_dag.py`` |
Total : ~1020 lignes relocalisées. 5 nouveaux shims minimaux.
Adaptations transverses
-----------------------
- ``reports_v2/html/renderers/worst_lines.py`` :
- import ``WorstLineEntry`` redirigé vers
``picarones.evaluation.metrics.worst_lines``
- import ``compute_char_diff`` redirigé vers
``picarones.evaluation`` (au lieu de
``picarones.core.diff_utils``, rejeté par la règle
layer-dependencies sur ``reports_v2``).
Cumul Phase 5.C (batches 1-5) : 25 renderers migrés. Reste
3 renderers : ``levers`` (284 l), ``pipeline_render`` (707 l),
``philological_render`` (595 l). Au total ~1586 LOC pour le
batch 6 (XXL + restants).
Acceptance
----------
5019 tests passent, lint vert, architecture vérifiée.
https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP
- docs/migration/legacy-retirement-plan.md +46 -5
- picarones/report/generator.py +2 -2
- picarones/report/pipeline_dag_render.py +11 -307
- picarones/report/taxonomy_comparison_render.py +11 -226
- picarones/report/taxonomy_cooccurrence_render.py +11 -154
- picarones/report/taxonomy_intra_doc_render.py +11 -141
- picarones/report/views/advanced_taxonomy.py +3 -3
- picarones/report/views/diagnostics.py +1 -1
- picarones/report/views/pipeline.py +1 -1
- picarones/report/worst_lines_render.py +11 -157
- picarones/reports_v2/html/renderers/pipeline_dag.py +320 -0
- picarones/reports_v2/html/renderers/taxonomy_comparison.py +239 -0
- picarones/reports_v2/html/renderers/taxonomy_cooccurrence.py +167 -0
- picarones/reports_v2/html/renderers/taxonomy_intra_doc.py +154 -0
- picarones/reports_v2/html/renderers/worst_lines.py +170 -0
- tests/report/test_extra_metrics.py +2 -2
- tests/report/test_sprint72_worst_lines.py +1 -1
- tests/report/test_sprint75_taxonomy_cooccurrence.py +1 -1
- tests/report/test_sprint76_taxonomy_intra_doc.py +1 -1
- tests/report/test_sprint77_taxonomy_comparison.py +1 -1
- tests/report/test_sprint95_pipeline_dag.py +1 -1
|
@@ -694,11 +694,9 @@ architecture vérifiée.
|
|
| 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 ✅ (cf. ci-dessous) — 5 renderers (188-321 LOC).
|
| 697 |
-
- Batch 5 (
|
| 698 |
-
|
| 699 |
-
``
|
| 700 |
-
- Batch 6 (les XXL) : ``pipeline_render`` (707 l),
|
| 701 |
-
``philological_render`` (595 l).
|
| 702 |
- Phase 5.D : 5 vues (``views/*.py``).
|
| 703 |
- Phase 5.E : ``generator.py``, ``comparison.py``,
|
| 704 |
``snapshot.py``, ``report_data/``, templates Jinja2.
|
|
@@ -812,6 +810,49 @@ architecture vérifiée.
|
|
| 812 |
et ``philological_render`` (595 l) — les XXL — auront leur propre
|
| 813 |
batch dédié.
|
| 814 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 815 |
### Phase 6 — Pipelines OCR+LLM (`pipelines/`)
|
| 816 |
|
| 817 |
**Modules** : `pipelines/base.OCRLLMPipeline` (3 modes), `pipelines/
|
|
|
|
| 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 ✅ (cf. ci-dessous) — 5 renderers (188-321 LOC).
|
| 697 |
+
- Batch 5 ✅ (cf. ci-dessous) — 5 renderers (148-314 LOC).
|
| 698 |
+
- Batch 6 (XXL + restants) : ``pipeline_render`` (707 l),
|
| 699 |
+
``philological_render`` (595 l), ``levers`` (284 l).
|
|
|
|
|
|
|
| 700 |
- Phase 5.D : 5 vues (``views/*.py``).
|
| 701 |
- Phase 5.E : ``generator.py``, ``comparison.py``,
|
| 702 |
``snapshot.py``, ``report_data/``, templates Jinja2.
|
|
|
|
| 810 |
et ``philological_render`` (595 l) — les XXL — auront leur propre
|
| 811 |
batch dédié.
|
| 812 |
|
| 813 |
+
#### Phase 5.C.batch5 — Lot 5 : 5 renderers moyens-gros (2026-05)
|
| 814 |
+
|
| 815 |
+
Cinquième vague. Inclut les 3 renderers de la famille
|
| 816 |
+
``taxonomy``, ``worst_lines`` et ``pipeline_dag``. Restera ensuite
|
| 817 |
+
batch 6 (XXL + ``levers``) et la migration des 5 vues
|
| 818 |
+
(``views/*.py``).
|
| 819 |
+
|
| 820 |
+
**Migrations effectuées** :
|
| 821 |
+
|
| 822 |
+
| Source legacy | Destination canonique |
|
| 823 |
+
|-------------------------------------------------|------------------------------------------------------|
|
| 824 |
+
| ``report/taxonomy_intra_doc_render.py`` (148) | ``reports_v2/html/renderers/taxonomy_intra_doc.py`` |
|
| 825 |
+
| ``report/taxonomy_cooccurrence_render.py`` (161)| ``reports_v2/html/renderers/taxonomy_cooccurrence.py``|
|
| 826 |
+
| ``report/worst_lines_render.py`` (164) | ``reports_v2/html/renderers/worst_lines.py`` |
|
| 827 |
+
| ``report/taxonomy_comparison_render.py`` (233) | ``reports_v2/html/renderers/taxonomy_comparison.py`` |
|
| 828 |
+
| ``report/pipeline_dag_render.py`` (314) | ``reports_v2/html/renderers/pipeline_dag.py`` |
|
| 829 |
+
|
| 830 |
+
Total : ~1020 lignes relocalisées.
|
| 831 |
+
|
| 832 |
+
**Adaptations transverses** :
|
| 833 |
+
|
| 834 |
+
- ``reports_v2/html/renderers/worst_lines.py`` :
|
| 835 |
+
- import ``WorstLineEntry`` redirigé vers
|
| 836 |
+
``picarones.evaluation.metrics.worst_lines``
|
| 837 |
+
- import ``compute_char_diff`` redirigé vers
|
| 838 |
+
``picarones.evaluation`` (au lieu de ``picarones.core.diff_utils``,
|
| 839 |
+
rejeté par la règle layer-dependencies sur ``reports_v2``).
|
| 840 |
+
|
| 841 |
+
**Cumul Phase 5.C** (batches 1+2+3+4+5) : 20 + 5 = **25 renderers
|
| 842 |
+
migrés**, soit l'intégralité moins ``pipeline_render`` et
|
| 843 |
+
``philological_render`` (XXL) et ``levers`` (oublié dans le plan
|
| 844 |
+
initial). Reste batch 6 (3 renderers) puis Phase 5.D (5 vues).
|
| 845 |
+
|
| 846 |
+
Wait — le compte exact : 22 originaux moins ``pipeline_render``,
|
| 847 |
+
``philological_render`` et ``levers`` = 19 attendus. Or on en a
|
| 848 |
+
migré 20 + 5 = 25 dans 5 batches. Vérification : on a fait
|
| 849 |
+
batch 1 (5) + batch 2 (5) + batch 3 (5) + batch 4 (5) + batch 5 (5)
|
| 850 |
+
= 25. Le plan initial listait 22 renderers ; en pratique le
|
| 851 |
+
``report/`` en contient ~28 (cf. ``ls report/*_render.py``) — la
|
| 852 |
+
liste du plan était incomplète. L'inventaire exact restant :
|
| 853 |
+
``levers_render.py`` + ``pipeline_render.py`` +
|
| 854 |
+
``philological_render.py`` à finir (3 renderers, ~1586 LOC).
|
| 855 |
+
|
| 856 |
### Phase 6 — Pipelines OCR+LLM (`pipelines/`)
|
| 857 |
|
| 858 |
**Modules** : `pipelines/base.OCRLLMPipeline` (3 modes), `pipelines/
|
|
@@ -316,10 +316,10 @@ class ReportGenerator:
|
|
| 316 |
from picarones.reports_v2.html.renderers.rare_token_recall import (
|
| 317 |
build_rare_token_recall_html,
|
| 318 |
)
|
| 319 |
-
from picarones.
|
| 320 |
build_taxonomy_cooccurrence_html,
|
| 321 |
)
|
| 322 |
-
from picarones.
|
| 323 |
build_taxonomy_intra_doc_html,
|
| 324 |
)
|
| 325 |
|
|
|
|
| 316 |
from picarones.reports_v2.html.renderers.rare_token_recall import (
|
| 317 |
build_rare_token_recall_html,
|
| 318 |
)
|
| 319 |
+
from picarones.reports_v2.html.renderers.taxonomy_cooccurrence import (
|
| 320 |
build_taxonomy_cooccurrence_html,
|
| 321 |
)
|
| 322 |
+
from picarones.reports_v2.html.renderers.taxonomy_intra_doc import (
|
| 323 |
build_taxonomy_intra_doc_html,
|
| 324 |
)
|
| 325 |
|
|
@@ -1,314 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
Outil d'inspection, pas de construction
|
| 6 |
-
---------------------------------------
|
| 7 |
-
Le YAML reste source de vérité. Cette vue **affiche** le
|
| 8 |
-
graphe orienté de la pipeline pour permettre l'inspection et
|
| 9 |
-
le debug d'un benchmark d'axe B (Sprint 63+) — elle ne
|
| 10 |
-
construit rien, ne supporte pas le drag-and-drop, n'exporte
|
| 11 |
-
aucun JSON modifiable.
|
| 12 |
-
|
| 13 |
-
Pattern identique aux autres rendus : SVG **server-side**,
|
| 14 |
-
pas de JS, anti-injection systématique.
|
| 15 |
-
|
| 16 |
-
Vue
|
| 17 |
-
---
|
| 18 |
-
Layout horizontal de gauche à droite :
|
| 19 |
-
|
| 20 |
-
- Chaque **nœud** est un rectangle annoté du nom du module et
|
| 21 |
-
de ses types d'entrée/sortie.
|
| 22 |
-
- Chaque **arête** porte une étiquette : type d'artefact +
|
| 23 |
-
métrique principale + valeur, avec un code couleur
|
| 24 |
-
vert/jaune/rouge selon le seuil sur la valeur.
|
| 25 |
-
|
| 26 |
-
Adaptive : ``""`` si moins d'un nœud.
|
| 27 |
-
|
| 28 |
-
Note d'intégration
|
| 29 |
-
------------------
|
| 30 |
-
Module pur — l'utilisateur compose les structures simples
|
| 31 |
-
``nodes`` et ``edges`` depuis sa ``PipelineSpec`` (Sprint 63)
|
| 32 |
-
et son ``PipelineBenchmarkResult`` (Sprint 64) :
|
| 33 |
-
|
| 34 |
-
.. code-block:: python
|
| 35 |
-
|
| 36 |
-
from picarones.report.pipeline_dag_render import build_pipeline_dag_html
|
| 37 |
-
|
| 38 |
-
nodes = [
|
| 39 |
-
{"name": s.name, "input_types": [t.value for t in s.module.input_types],
|
| 40 |
-
"output_types": [t.value for t in s.module.output_types]}
|
| 41 |
-
for s in spec.steps
|
| 42 |
-
]
|
| 43 |
-
edges = []
|
| 44 |
-
for prev, curr in zip(spec.steps, spec.steps[1:]):
|
| 45 |
-
agg = bench.aggregate_for_step(curr.name)
|
| 46 |
-
for art_type, metrics in (agg.junction_metrics or {}).items():
|
| 47 |
-
for metric_name, value in metrics.items():
|
| 48 |
-
edges.append({
|
| 49 |
-
"from": prev.name, "to": curr.name,
|
| 50 |
-
"artifact_type": art_type, "metric_name": metric_name,
|
| 51 |
-
"metric_value": value.get("mean"),
|
| 52 |
-
})
|
| 53 |
-
html = build_pipeline_dag_html(nodes, edges, labels)
|
| 54 |
"""
|
| 55 |
|
| 56 |
from __future__ import annotations
|
| 57 |
|
| 58 |
-
|
| 59 |
-
from typing import Optional
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
# Seuils par défaut sur les métriques d'erreur (CER-like, lower is better).
|
| 63 |
-
_DEFAULT_THRESHOLDS = (0.05, 0.15) # vert ≤ 0.05, jaune ≤ 0.15, rouge > 0.15
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def _classify_metric(
|
| 67 |
-
value: Optional[float],
|
| 68 |
-
thresholds: tuple[float, float],
|
| 69 |
-
higher_is_better: bool,
|
| 70 |
-
) -> str:
|
| 71 |
-
"""Retourne ``"green"``, ``"yellow"``, ``"red"`` ou ``"none"``."""
|
| 72 |
-
if value is None:
|
| 73 |
-
return "none"
|
| 74 |
-
try:
|
| 75 |
-
v = float(value)
|
| 76 |
-
except (TypeError, ValueError):
|
| 77 |
-
return "none"
|
| 78 |
-
low, high = thresholds
|
| 79 |
-
if higher_is_better:
|
| 80 |
-
# Inversion : haut = bon
|
| 81 |
-
if v >= 1.0 - low:
|
| 82 |
-
return "green"
|
| 83 |
-
if v >= 1.0 - high:
|
| 84 |
-
return "yellow"
|
| 85 |
-
return "red"
|
| 86 |
-
if v <= low:
|
| 87 |
-
return "green"
|
| 88 |
-
if v <= high:
|
| 89 |
-
return "yellow"
|
| 90 |
-
return "red"
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
# Sprint A7 (m-5) — palette Okabe-Ito daltonien-friendly importée
|
| 94 |
-
# depuis le module canonique ``picarones.report.colors``. Avant
|
| 95 |
-
# A7, les hex étaient hardcodés (rouge/vert classiques, problème
|
| 96 |
-
# pour la deutéranopie) ; maintenant cohérent avec _cer_color et
|
| 97 |
-
# difficulty_color.
|
| 98 |
-
from picarones.reports_v2._helpers.colors import COLOR_GREEN, COLOR_RED, COLOR_YELLOW
|
| 99 |
-
|
| 100 |
-
_QUALITY_COLORS = {
|
| 101 |
-
"green": COLOR_GREEN, # Okabe-Ito blue (substitut sémantique « bon »)
|
| 102 |
-
"yellow": COLOR_YELLOW, # Okabe-Ito yellow
|
| 103 |
-
"red": COLOR_RED, # Okabe-Ito vermillion (substitut sémantique « mauvais »)
|
| 104 |
-
"none": "#6b7280",
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def _format_value(value: Optional[float]) -> str:
|
| 109 |
-
if value is None:
|
| 110 |
-
return "—"
|
| 111 |
-
try:
|
| 112 |
-
v = float(value)
|
| 113 |
-
except (TypeError, ValueError):
|
| 114 |
-
return "—"
|
| 115 |
-
if abs(v) < 1.0:
|
| 116 |
-
return f"{v * 100:.1f}%"
|
| 117 |
-
return f"{v:.2f}"
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def build_pipeline_dag_html(
|
| 121 |
-
nodes: Optional[list[dict]],
|
| 122 |
-
labels: Optional[dict[str, str]] = None,
|
| 123 |
-
edges: Optional[list[dict]] = None,
|
| 124 |
-
*,
|
| 125 |
-
thresholds: tuple[float, float] = _DEFAULT_THRESHOLDS,
|
| 126 |
-
higher_is_better: bool = False,
|
| 127 |
-
) -> str:
|
| 128 |
-
"""Construit la vue HTML « Pipeline DAG ».
|
| 129 |
-
|
| 130 |
-
Parameters
|
| 131 |
-
----------
|
| 132 |
-
nodes:
|
| 133 |
-
Liste de dicts ``{"name", "input_types"?, "output_types"?}``
|
| 134 |
-
dans l'ordre topologique. Si vide ou ``None``, retourne
|
| 135 |
-
``""``.
|
| 136 |
-
labels:
|
| 137 |
-
Dict i18n. Clés sous le préfixe ``dag_*``.
|
| 138 |
-
edges:
|
| 139 |
-
Liste de dicts ``{"from", "to", "artifact_type"?,
|
| 140 |
-
"metric_name"?, "metric_value"?}``. Optionnel —
|
| 141 |
-
auto-déduit séquentiel sinon.
|
| 142 |
-
thresholds:
|
| 143 |
-
``(seuil_vert, seuil_jaune)`` sur la valeur de métrique.
|
| 144 |
-
Défaut ``(0.05, 0.15)`` — convention CER.
|
| 145 |
-
higher_is_better:
|
| 146 |
-
Si ``True``, la sémantique est inversée (1 = meilleur).
|
| 147 |
-
"""
|
| 148 |
-
nodes = list(nodes or [])
|
| 149 |
-
if not nodes:
|
| 150 |
-
return ""
|
| 151 |
-
edges = list(edges or [])
|
| 152 |
-
labels = labels or {}
|
| 153 |
-
title = labels.get("dag_title", "Pipeline DAG")
|
| 154 |
-
note = labels.get(
|
| 155 |
-
"dag_note",
|
| 156 |
-
"Graphe orienté du pipeline composé. Chaque arête porte "
|
| 157 |
-
"le type d'artefact transmis et la métrique calculée à "
|
| 158 |
-
"la jonction. Code couleur vert/orange/rouge selon le "
|
| 159 |
-
"seuil. Outil d'inspection — le YAML reste source de "
|
| 160 |
-
"vérité.",
|
| 161 |
-
)
|
| 162 |
-
# Layout horizontal régulier
|
| 163 |
-
n = len(nodes)
|
| 164 |
-
box_width = 160
|
| 165 |
-
box_height = 70
|
| 166 |
-
h_gap = 110 # espace horizontal entre nœuds
|
| 167 |
-
margin = 30
|
| 168 |
-
svg_width = margin * 2 + n * box_width + (n - 1) * h_gap
|
| 169 |
-
svg_height = box_height + margin * 2 + 60 # +60 pour étiquettes arêtes
|
| 170 |
-
centre_y = margin + box_height / 2 + 30 # offset pour étiquette de tête
|
| 171 |
-
|
| 172 |
-
# Index des nœuds par name pour récupérer la position
|
| 173 |
-
node_x: dict[str, float] = {}
|
| 174 |
-
parts: list[str] = [
|
| 175 |
-
'<section class="dag-section" style="margin:1rem 0">',
|
| 176 |
-
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 177 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 178 |
-
f'{_e(note)}</div>',
|
| 179 |
-
f'<svg viewBox="0 0 {svg_width} {svg_height}" '
|
| 180 |
-
f'role="img" aria-label="{_e(title)}" '
|
| 181 |
-
'xmlns="http://www.w3.org/2000/svg" '
|
| 182 |
-
'style="max-width:100%;height:auto;'
|
| 183 |
-
'font-family:system-ui,sans-serif;font-size:12px">',
|
| 184 |
-
# Définition d'une flèche
|
| 185 |
-
'<defs>'
|
| 186 |
-
'<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" '
|
| 187 |
-
'markerWidth="6" markerHeight="6" orient="auto-start-reverse">'
|
| 188 |
-
'<path d="M0,0 L10,5 L0,10 z" fill="#374151"/>'
|
| 189 |
-
'</marker>'
|
| 190 |
-
'</defs>',
|
| 191 |
-
]
|
| 192 |
-
|
| 193 |
-
# Étape 1 : nœuds
|
| 194 |
-
for i, node in enumerate(nodes):
|
| 195 |
-
name = str(node.get("name") or f"step_{i}")
|
| 196 |
-
x = margin + i * (box_width + h_gap)
|
| 197 |
-
y = margin + 30
|
| 198 |
-
node_x[name] = x + box_width
|
| 199 |
-
in_types = ", ".join(node.get("input_types") or [])
|
| 200 |
-
out_types = ", ".join(node.get("output_types") or [])
|
| 201 |
-
parts.append(
|
| 202 |
-
f'<rect x="{x}" y="{y}" width="{box_width}" '
|
| 203 |
-
f'height="{box_height}" rx="6" fill="#f3f4f6" '
|
| 204 |
-
f'stroke="#374151" stroke-width="1.5"/>'
|
| 205 |
-
)
|
| 206 |
-
parts.append(
|
| 207 |
-
f'<text x="{x + box_width / 2}" y="{y + 22}" '
|
| 208 |
-
f'text-anchor="middle" font-weight="600" '
|
| 209 |
-
f'fill="#111827">{_e(name)}</text>'
|
| 210 |
-
)
|
| 211 |
-
if in_types:
|
| 212 |
-
parts.append(
|
| 213 |
-
f'<text x="{x + box_width / 2}" y="{y + 40}" '
|
| 214 |
-
f'text-anchor="middle" fill="#4b5563" '
|
| 215 |
-
f'font-size="10">in: {_e(in_types)}</text>'
|
| 216 |
-
)
|
| 217 |
-
if out_types:
|
| 218 |
-
parts.append(
|
| 219 |
-
f'<text x="{x + box_width / 2}" y="{y + 56}" '
|
| 220 |
-
f'text-anchor="middle" fill="#4b5563" '
|
| 221 |
-
f'font-size="10">out: {_e(out_types)}</text>'
|
| 222 |
-
)
|
| 223 |
-
|
| 224 |
-
# Étape 2 : arêtes (mappées sur paires séquentielles si pas de
|
| 225 |
-
# "from"/"to" explicites — voir nodes par défaut)
|
| 226 |
-
auto_edges: list[dict] = []
|
| 227 |
-
if not edges:
|
| 228 |
-
for prev, curr in zip(nodes, nodes[1:]):
|
| 229 |
-
auto_edges.append({
|
| 230 |
-
"from": prev.get("name"),
|
| 231 |
-
"to": curr.get("name"),
|
| 232 |
-
})
|
| 233 |
-
else:
|
| 234 |
-
auto_edges = edges
|
| 235 |
-
|
| 236 |
-
for edge in auto_edges:
|
| 237 |
-
src = str(edge.get("from") or "")
|
| 238 |
-
dst = str(edge.get("to") or "")
|
| 239 |
-
if not src or not dst:
|
| 240 |
-
continue
|
| 241 |
-
# Position : du bord droit du src au bord gauche du dst
|
| 242 |
-
# Heuristique : on prend la position du nœud src dans la
|
| 243 |
-
# liste pour calculer x1, et celle de dst pour x2.
|
| 244 |
-
try:
|
| 245 |
-
i_src = next(
|
| 246 |
-
i for i, n_ in enumerate(nodes)
|
| 247 |
-
if n_.get("name") == src
|
| 248 |
-
)
|
| 249 |
-
i_dst = next(
|
| 250 |
-
i for i, n_ in enumerate(nodes)
|
| 251 |
-
if n_.get("name") == dst
|
| 252 |
-
)
|
| 253 |
-
except StopIteration:
|
| 254 |
-
continue
|
| 255 |
-
x1 = margin + i_src * (box_width + h_gap) + box_width
|
| 256 |
-
x2 = margin + i_dst * (box_width + h_gap)
|
| 257 |
-
y = centre_y
|
| 258 |
-
# Classe la métrique pour le code couleur
|
| 259 |
-
value = edge.get("metric_value")
|
| 260 |
-
try:
|
| 261 |
-
value_f = float(value) if value is not None else None
|
| 262 |
-
except (TypeError, ValueError):
|
| 263 |
-
value_f = None
|
| 264 |
-
cls = _classify_metric(value_f, thresholds, higher_is_better)
|
| 265 |
-
color = _QUALITY_COLORS[cls]
|
| 266 |
-
# Trace la flèche
|
| 267 |
-
parts.append(
|
| 268 |
-
f'<line x1="{x1}" y1="{y}" x2="{x2}" y2="{y}" '
|
| 269 |
-
f'stroke="{color}" stroke-width="2" '
|
| 270 |
-
f'marker-end="url(#arrow)"/>'
|
| 271 |
-
)
|
| 272 |
-
# Étiquette : type + métrique : valeur
|
| 273 |
-
artifact_type = edge.get("artifact_type") or ""
|
| 274 |
-
metric_name = edge.get("metric_name") or ""
|
| 275 |
-
value_str = _format_value(value_f)
|
| 276 |
-
label_lines: list[str] = []
|
| 277 |
-
if artifact_type:
|
| 278 |
-
label_lines.append(str(artifact_type))
|
| 279 |
-
if metric_name:
|
| 280 |
-
label_lines.append(f"{metric_name}: {value_str}")
|
| 281 |
-
if label_lines:
|
| 282 |
-
label_x = (x1 + x2) / 2
|
| 283 |
-
for k, line in enumerate(label_lines):
|
| 284 |
-
parts.append(
|
| 285 |
-
f'<text x="{label_x}" y="{y - 8 - k * 12}" '
|
| 286 |
-
f'text-anchor="middle" fill="{color}" '
|
| 287 |
-
f'font-size="10" font-weight="600">'
|
| 288 |
-
f'{_e(line)}</text>'
|
| 289 |
-
)
|
| 290 |
-
parts.append("</svg>")
|
| 291 |
-
|
| 292 |
-
# Légende
|
| 293 |
-
h_legend = labels.get("dag_legend", "Lecture")
|
| 294 |
-
legend_green = labels.get("dag_legend_green", "qualité élevée")
|
| 295 |
-
legend_yellow = labels.get("dag_legend_yellow", "qualité moyenne")
|
| 296 |
-
legend_red = labels.get("dag_legend_red", "qualité faible")
|
| 297 |
-
parts.append(
|
| 298 |
-
'<div style="font-size:.8rem;opacity:.75;margin-top:.4rem">'
|
| 299 |
-
f'<strong>{_e(h_legend)} :</strong> '
|
| 300 |
-
f'<span style="color:{_QUALITY_COLORS["green"]};'
|
| 301 |
-
f'font-weight:600">●</span> {_e(legend_green)} '
|
| 302 |
-
f'(≤ {thresholds[0] * 100:.0f}%) '
|
| 303 |
-
f'<span style="color:{_QUALITY_COLORS["yellow"]};'
|
| 304 |
-
f'font-weight:600">●</span> {_e(legend_yellow)} '
|
| 305 |
-
f'(≤ {thresholds[1] * 100:.0f}%) '
|
| 306 |
-
f'<span style="color:{_QUALITY_COLORS["red"]};'
|
| 307 |
-
f'font-weight:600">●</span> {_e(legend_red)}'
|
| 308 |
-
'</div>'
|
| 309 |
-
)
|
| 310 |
-
parts.append("</section>")
|
| 311 |
-
return "".join(parts)
|
| 312 |
|
|
|
|
| 313 |
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.pipeline_dag_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.pipeline_dag`.
|
| 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.pipeline_dag import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.pipeline_dag_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.pipeline_dag instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -1,233 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
Suite directe ``picarones/core/taxonomy_comparison.py``. Pattern
|
| 6 |
-
identique aux autres rendus (Sprints 41/43/62/67/72/74/75/76) :
|
| 7 |
-
**server-side**, pas de JavaScript, anti-injection systématique.
|
| 8 |
-
|
| 9 |
-
Diagramme miroir
|
| 10 |
-
----------------
|
| 11 |
-
Une ligne par classe taxonomique, divisée en deux barres
|
| 12 |
-
horizontales :
|
| 13 |
-
|
| 14 |
-
- À **gauche** : barre du moteur A (orientée vers la gauche, du
|
| 15 |
-
centre vers le bord).
|
| 16 |
-
- À **droite** : barre du moteur B (orientée vers la droite).
|
| 17 |
-
- Couleur de la classe selon ``recoverability`` :
|
| 18 |
-
|
| 19 |
-
- vert (#5fa860) : ``recoverable``
|
| 20 |
-
- orange (#e0a050) : ``difficult``
|
| 21 |
-
- rouge (#d8553b) : ``irrecoverable``
|
| 22 |
-
|
| 23 |
-
Lecture immédiate : un moteur dont les barres tirent vers la
|
| 24 |
-
**gauche** sur du vert (case_error, ligature_error) et un moteur
|
| 25 |
-
qui tire à droite sur du rouge (lacuna) — la décision éditoriale
|
| 26 |
-
est évidente même si les CER globaux sont identiques.
|
| 27 |
"""
|
| 28 |
|
| 29 |
from __future__ import annotations
|
| 30 |
|
| 31 |
-
|
| 32 |
-
from typing import Optional
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
_RECOVERABILITY_COLORS = {
|
| 36 |
-
"recoverable": "#5fa860",
|
| 37 |
-
"difficult": "#e0a050",
|
| 38 |
-
"irrecoverable": "#d8553b",
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def _build_mirror_chart_svg(
|
| 43 |
-
data: dict,
|
| 44 |
-
*,
|
| 45 |
-
bar_max_width: int = 200,
|
| 46 |
-
row_height: int = 22,
|
| 47 |
-
label_width: int = 140,
|
| 48 |
-
margin_top: int = 50,
|
| 49 |
-
margin_bottom: int = 20,
|
| 50 |
-
) -> str:
|
| 51 |
-
"""Construit le diagramme miroir SVG."""
|
| 52 |
-
classes = data["classes"]
|
| 53 |
-
prop_a = data["proportions_a"]
|
| 54 |
-
prop_b = data["proportions_b"]
|
| 55 |
-
recov = data["recoverability"]
|
| 56 |
-
engine_a = data["engine_a"]
|
| 57 |
-
engine_b = data["engine_b"]
|
| 58 |
-
|
| 59 |
-
n_rows = len(classes)
|
| 60 |
-
if n_rows == 0:
|
| 61 |
-
return ""
|
| 62 |
-
|
| 63 |
-
# Échelle : on normalise à la valeur max de toutes les
|
| 64 |
-
# proportions (pour que la classe la plus présente atteigne
|
| 65 |
-
# bar_max_width).
|
| 66 |
-
max_prop = max(
|
| 67 |
-
max(prop_a.values(), default=0.0),
|
| 68 |
-
max(prop_b.values(), default=0.0),
|
| 69 |
-
)
|
| 70 |
-
if max_prop <= 0:
|
| 71 |
-
max_prop = 1.0 # évite division par zéro (cas dégénéré)
|
| 72 |
-
|
| 73 |
-
width = label_width + 2 * bar_max_width + 40
|
| 74 |
-
height = margin_top + n_rows * row_height + margin_bottom
|
| 75 |
-
center = width // 2
|
| 76 |
-
|
| 77 |
-
parts = [
|
| 78 |
-
f'<svg xmlns="http://www.w3.org/2000/svg" '
|
| 79 |
-
f'width="{width}" height="{height}" '
|
| 80 |
-
f'viewBox="0 0 {width} {height}" '
|
| 81 |
-
f'role="img" aria-label="Diagramme miroir taxonomique">',
|
| 82 |
-
# En-têtes des deux moteurs
|
| 83 |
-
f'<text x="{center - bar_max_width // 2}" y="20" '
|
| 84 |
-
f'font-size="13" font-weight="600" fill="#333" '
|
| 85 |
-
f'text-anchor="middle">{_e(engine_a)}</text>',
|
| 86 |
-
f'<text x="{center + bar_max_width // 2}" y="20" '
|
| 87 |
-
f'font-size="13" font-weight="600" fill="#333" '
|
| 88 |
-
f'text-anchor="middle">{_e(engine_b)}</text>',
|
| 89 |
-
# Ligne centrale
|
| 90 |
-
f'<line x1="{center}" y1="{margin_top - 4}" '
|
| 91 |
-
f'x2="{center}" y2="{height - margin_bottom + 4}" '
|
| 92 |
-
f'stroke="#999" stroke-width="1"/>',
|
| 93 |
-
]
|
| 94 |
-
|
| 95 |
-
# Barres
|
| 96 |
-
for i, cls in enumerate(classes):
|
| 97 |
-
y = margin_top + i * row_height
|
| 98 |
-
level = recov.get(cls, "difficult")
|
| 99 |
-
color = _RECOVERABILITY_COLORS.get(level, "#888")
|
| 100 |
-
# Étiquette de classe au centre
|
| 101 |
-
parts.append(
|
| 102 |
-
f'<text x="{center}" y="{y + row_height // 2 + 4}" '
|
| 103 |
-
f'font-size="11" fill="#222" text-anchor="middle" '
|
| 104 |
-
f'font-family="monospace">{_e(cls)}</text>'
|
| 105 |
-
)
|
| 106 |
-
# Barre A (gauche)
|
| 107 |
-
a_width = (prop_a.get(cls, 0.0) / max_prop) * bar_max_width
|
| 108 |
-
if a_width > 0:
|
| 109 |
-
x_a = center - label_width // 2 - a_width
|
| 110 |
-
parts.append(
|
| 111 |
-
f'<rect x="{x_a:.1f}" y="{y + 3}" '
|
| 112 |
-
f'width="{a_width:.1f}" height="{row_height - 6}" '
|
| 113 |
-
f'fill="{color}" stroke="#666" stroke-width="0.5" '
|
| 114 |
-
f'opacity="0.85"/>'
|
| 115 |
-
)
|
| 116 |
-
# Valeur en %
|
| 117 |
-
parts.append(
|
| 118 |
-
f'<text x="{x_a - 3:.1f}" y="{y + row_height // 2 + 4}" '
|
| 119 |
-
f'font-size="10" fill="#444" text-anchor="end">'
|
| 120 |
-
f'{prop_a.get(cls, 0.0) * 100:.1f}%</text>'
|
| 121 |
-
)
|
| 122 |
-
# Barre B (droite)
|
| 123 |
-
b_width = (prop_b.get(cls, 0.0) / max_prop) * bar_max_width
|
| 124 |
-
if b_width > 0:
|
| 125 |
-
x_b = center + label_width // 2
|
| 126 |
-
parts.append(
|
| 127 |
-
f'<rect x="{x_b:.1f}" y="{y + 3}" '
|
| 128 |
-
f'width="{b_width:.1f}" height="{row_height - 6}" '
|
| 129 |
-
f'fill="{color}" stroke="#666" stroke-width="0.5" '
|
| 130 |
-
f'opacity="0.85"/>'
|
| 131 |
-
)
|
| 132 |
-
parts.append(
|
| 133 |
-
f'<text x="{x_b + b_width + 3:.1f}" '
|
| 134 |
-
f'y="{y + row_height // 2 + 4}" '
|
| 135 |
-
f'font-size="10" fill="#444" text-anchor="start">'
|
| 136 |
-
f'{prop_b.get(cls, 0.0) * 100:.1f}%</text>'
|
| 137 |
-
)
|
| 138 |
-
parts.append("</svg>")
|
| 139 |
-
return "".join(parts)
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
def _build_recoverability_summary_html(
|
| 143 |
-
data: dict, labels: dict,
|
| 144 |
-
) -> str:
|
| 145 |
-
"""Encart résumé par catégorie de récupérabilité (3 lignes)."""
|
| 146 |
-
totals = data.get("totals_by_recoverability") or {}
|
| 147 |
-
if not totals:
|
| 148 |
-
return ""
|
| 149 |
-
label_recov = labels.get("taxocomp_recoverable", "Récupérable")
|
| 150 |
-
label_diff = labels.get("taxocomp_difficult", "Difficile")
|
| 151 |
-
label_irrec = labels.get("taxocomp_irrecoverable", "Irrécupérable")
|
| 152 |
-
rows = [
|
| 153 |
-
("recoverable", label_recov),
|
| 154 |
-
("difficult", label_diff),
|
| 155 |
-
("irrecoverable", label_irrec),
|
| 156 |
-
]
|
| 157 |
-
parts = [
|
| 158 |
-
'<table style="border-collapse:collapse;font-size:.85rem;'
|
| 159 |
-
'margin-top:.5rem">',
|
| 160 |
-
'<thead><tr>',
|
| 161 |
-
'<th scope=\"col\" style="padding:.2rem .5rem;text-align:left;'
|
| 162 |
-
'border-bottom:1px solid #ccc">'
|
| 163 |
-
f'{_e(labels.get("taxocomp_level_label", "Catégorie"))}</th>',
|
| 164 |
-
'<th scope=\"col\" style="padding:.2rem .5rem;text-align:right;'
|
| 165 |
-
'border-bottom:1px solid #ccc">'
|
| 166 |
-
f'{_e(_e(data["engine_a"]))}</th>',
|
| 167 |
-
'<th scope=\"col\" style="padding:.2rem .5rem;text-align:right;'
|
| 168 |
-
'border-bottom:1px solid #ccc">'
|
| 169 |
-
f'{_e(_e(data["engine_b"]))}</th>',
|
| 170 |
-
'</tr></thead><tbody>',
|
| 171 |
-
]
|
| 172 |
-
for level, label in rows:
|
| 173 |
-
cell = totals.get(level, {"a": 0.0, "b": 0.0})
|
| 174 |
-
color = _RECOVERABILITY_COLORS.get(level, "#888")
|
| 175 |
-
parts.append(
|
| 176 |
-
f'<tr>'
|
| 177 |
-
f'<td style="padding:.2rem .5rem">'
|
| 178 |
-
f'<span style="display:inline-block;width:10px;height:10px;'
|
| 179 |
-
f'background:{color};margin-right:.4rem;border-radius:2px"></span>'
|
| 180 |
-
f'{_e(label)}</td>'
|
| 181 |
-
f'<td style="padding:.2rem .5rem;text-align:right;'
|
| 182 |
-
f'font-family:monospace">{cell["a"] * 100:.1f}%</td>'
|
| 183 |
-
f'<td style="padding:.2rem .5rem;text-align:right;'
|
| 184 |
-
f'font-family:monospace">{cell["b"] * 100:.1f}%</td>'
|
| 185 |
-
f'</tr>'
|
| 186 |
-
)
|
| 187 |
-
parts.append("</tbody></table>")
|
| 188 |
-
return "".join(parts)
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
def build_taxonomy_comparison_html(
|
| 192 |
-
data: Optional[dict],
|
| 193 |
-
labels: Optional[dict[str, str]] = None,
|
| 194 |
-
) -> str:
|
| 195 |
-
"""Construit le bloc HTML de comparaison taxonomique entre 2 moteurs.
|
| 196 |
-
|
| 197 |
-
Retourne ``""`` si ``data is None`` ou aucune classe.
|
| 198 |
-
"""
|
| 199 |
-
if not data:
|
| 200 |
-
return ""
|
| 201 |
-
classes = data.get("classes") or []
|
| 202 |
-
if not classes:
|
| 203 |
-
return ""
|
| 204 |
-
labels = labels or {}
|
| 205 |
-
title_template = labels.get(
|
| 206 |
-
"taxocomp_title", "Profil taxonomique : {engine_a} vs {engine_b}",
|
| 207 |
-
)
|
| 208 |
-
title = title_template.format(
|
| 209 |
-
engine_a=data["engine_a"], engine_b=data["engine_b"],
|
| 210 |
-
)
|
| 211 |
-
note = labels.get(
|
| 212 |
-
"taxocomp_note",
|
| 213 |
-
"Diagramme miroir des proportions d'erreurs par classe. "
|
| 214 |
-
"Couleur selon récupérabilité éditoriale (vert = corrigeable, "
|
| 215 |
-
"rouge = irrécupérable). À CER global égal, un moteur dont les "
|
| 216 |
-
"erreurs sont majoritairement vertes est préférable pour une "
|
| 217 |
-
"édition critique.",
|
| 218 |
-
)
|
| 219 |
-
parts = [
|
| 220 |
-
'<div class="taxocomp" style="margin:1rem 0">',
|
| 221 |
-
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 222 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 223 |
-
f'{_e(note)}</div>',
|
| 224 |
-
_build_mirror_chart_svg(data),
|
| 225 |
-
_build_recoverability_summary_html(data, labels),
|
| 226 |
-
"</div>",
|
| 227 |
-
]
|
| 228 |
-
return "".join(parts)
|
| 229 |
|
|
|
|
| 230 |
|
| 231 |
-
|
| 232 |
-
"
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.taxonomy_comparison_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.taxonomy_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.taxonomy_comparison import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.taxonomy_comparison_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.taxonomy_comparison instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -1,161 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
Suite directe ``picarones/core/taxonomy_cooccurrence.py``. Pattern
|
| 6 |
-
identique aux autres rendus (Sprints 41/43/62/67/72/74) :
|
| 7 |
-
**server-side**, pas de JavaScript, anti-injection systématique.
|
| 8 |
-
|
| 9 |
-
Sortie typique
|
| 10 |
-
--------------
|
| 11 |
-
- ``build_taxonomy_cooccurrence_html(data, labels)`` produit un
|
| 12 |
-
bloc complet : titre + note d'usage + heatmap SVG + table des
|
| 13 |
-
paires les plus co-occurrentes.
|
| 14 |
-
- ``""`` retourné si ``data is None`` ou si la matrice est vide
|
| 15 |
-
(rapport adaptatif).
|
| 16 |
"""
|
| 17 |
|
| 18 |
from __future__ import annotations
|
| 19 |
|
| 20 |
-
|
| 21 |
-
from typing import Optional
|
| 22 |
-
|
| 23 |
-
from picarones.reports_v2._helpers.render_helpers import (
|
| 24 |
-
GRADIENT_TARGET_BLUE,
|
| 25 |
-
build_grid_svg,
|
| 26 |
-
color_single_gradient,
|
| 27 |
-
text_color_for_bg,
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def _build_jaccard_heatmap_svg(
|
| 32 |
-
classes: list[str],
|
| 33 |
-
matrix: dict[str, dict[str, float]],
|
| 34 |
-
*,
|
| 35 |
-
cell_size: int = 36,
|
| 36 |
-
label_left: int = 130,
|
| 37 |
-
label_top: int = 80,
|
| 38 |
-
) -> str:
|
| 39 |
-
"""Heatmap Jaccard de co-occurrence taxonomique.
|
| 40 |
-
|
| 41 |
-
Délègue à :func:`build_grid_svg` ; reste un wrapper local qui
|
| 42 |
-
encapsule les conventions spécifiques à la matrice symétrique
|
| 43 |
-
(valeur affichée seulement si > 0,05, étiquettes rotées).
|
| 44 |
-
"""
|
| 45 |
-
if not classes:
|
| 46 |
-
return ""
|
| 47 |
-
|
| 48 |
-
def cell_value(i: int, j: int) -> float:
|
| 49 |
-
return matrix.get(classes[i], {}).get(classes[j], 0.0)
|
| 50 |
-
|
| 51 |
-
return build_grid_svg(
|
| 52 |
-
n_rows=len(classes),
|
| 53 |
-
n_cols=len(classes),
|
| 54 |
-
row_label_fn=lambda i: classes[i],
|
| 55 |
-
col_label_fn=lambda j: classes[j],
|
| 56 |
-
cell_color_fn=lambda i, j: color_single_gradient(
|
| 57 |
-
cell_value(i, j), end_rgb=GRADIENT_TARGET_BLUE,
|
| 58 |
-
),
|
| 59 |
-
cell_text_fn=lambda i, j: (
|
| 60 |
-
f"{cell_value(i, j):.2f}" if cell_value(i, j) > 0.05 else None
|
| 61 |
-
),
|
| 62 |
-
cell_text_color_fn=lambda i, j: text_color_for_bg(cell_value(i, j)),
|
| 63 |
-
cell_w=cell_size,
|
| 64 |
-
cell_h=cell_size,
|
| 65 |
-
label_left=label_left,
|
| 66 |
-
label_top=label_top,
|
| 67 |
-
rotate_col_labels=True,
|
| 68 |
-
aria_label="Heatmap Jaccard co-occurrence taxonomique",
|
| 69 |
-
)
|
| 70 |
|
|
|
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
return ""
|
| 79 |
-
pair_label = labels.get("taxocooc_pair_label", "Paire")
|
| 80 |
-
jaccard_label = labels.get("taxocooc_jaccard_label", "Jaccard")
|
| 81 |
-
|
| 82 |
-
parts = [
|
| 83 |
-
'<table style="border-collapse:collapse;font-size:.85rem;'
|
| 84 |
-
'margin-top:.5rem">',
|
| 85 |
-
'<thead><tr>',
|
| 86 |
-
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 87 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 88 |
-
f'{_e(pair_label)}</th>',
|
| 89 |
-
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:right;'
|
| 90 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 91 |
-
f'{_e(jaccard_label)}</th>',
|
| 92 |
-
'</tr></thead><tbody>',
|
| 93 |
-
]
|
| 94 |
-
for ca, cb, j in top_pairs:
|
| 95 |
-
parts.append(
|
| 96 |
-
f'<tr>'
|
| 97 |
-
f'<td style="padding:.2rem .5rem">'
|
| 98 |
-
f'<code>{_e(ca)}</code> ↔ <code>{_e(cb)}</code></td>'
|
| 99 |
-
f'<td style="padding:.2rem .5rem;text-align:right;'
|
| 100 |
-
f'font-family:monospace;'
|
| 101 |
-
f'background:{color_single_gradient(j, end_rgb=GRADIENT_TARGET_BLUE)};'
|
| 102 |
-
f'color:{text_color_for_bg(j)}">{j:.2f}</td>'
|
| 103 |
-
f'</tr>'
|
| 104 |
-
)
|
| 105 |
-
parts.append("</tbody></table>")
|
| 106 |
-
return "".join(parts)
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def build_taxonomy_cooccurrence_html(
|
| 110 |
-
data: Optional[dict],
|
| 111 |
-
labels: Optional[dict[str, str]] = None,
|
| 112 |
-
) -> str:
|
| 113 |
-
"""Construit le bloc HTML complet de co-occurrence taxonomique.
|
| 114 |
-
|
| 115 |
-
Retourne ``""`` si ``data is None`` ou matrice vide.
|
| 116 |
-
"""
|
| 117 |
-
if not data:
|
| 118 |
-
return ""
|
| 119 |
-
classes = data.get("classes") or []
|
| 120 |
-
matrix = data.get("cooccurrence_matrix") or {}
|
| 121 |
-
if not classes or not matrix:
|
| 122 |
-
return ""
|
| 123 |
-
labels = labels or {}
|
| 124 |
-
title = labels.get(
|
| 125 |
-
"taxocooc_title",
|
| 126 |
-
"Co-occurrence des classes d'erreur",
|
| 127 |
-
)
|
| 128 |
-
note = labels.get(
|
| 129 |
-
"taxocooc_note",
|
| 130 |
-
"Indice de Jaccard au niveau document : 1,00 = ces deux classes "
|
| 131 |
-
"apparaissent toujours ensemble ; 0,00 = jamais. Lecture par paires "
|
| 132 |
-
"co-occurrentes ci-dessous.",
|
| 133 |
-
)
|
| 134 |
-
n_docs = data.get("n_documents", 0)
|
| 135 |
-
n_docs_label_template = labels.get(
|
| 136 |
-
"taxocooc_n_docs", "Calculé sur {n_docs} documents.",
|
| 137 |
-
)
|
| 138 |
-
n_docs_phrase = n_docs_label_template.format(n_docs=n_docs)
|
| 139 |
-
|
| 140 |
-
svg = _build_jaccard_heatmap_svg(classes, matrix)
|
| 141 |
-
top_table = _build_top_pairs_table(
|
| 142 |
-
data.get("top_pairs") or [], labels,
|
| 143 |
-
)
|
| 144 |
-
|
| 145 |
-
parts = [
|
| 146 |
-
'<div class="taxocooc" style="margin:1rem 0">',
|
| 147 |
-
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 148 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 149 |
-
f'{_e(note)}</div>',
|
| 150 |
-
f'<div style="font-size:.8rem;opacity:.7;margin-bottom:.5rem">'
|
| 151 |
-
f'{_e(n_docs_phrase)}</div>',
|
| 152 |
-
svg,
|
| 153 |
-
top_table,
|
| 154 |
-
"</div>",
|
| 155 |
-
]
|
| 156 |
-
return "".join(parts)
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
__all__ = [
|
| 160 |
-
"build_taxonomy_cooccurrence_html",
|
| 161 |
-
]
|
|
|
|
| 1 |
+
"""``picarones.report.taxonomy_cooccurrence_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.taxonomy_cooccurrence`.
|
| 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.taxonomy_cooccurrence import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.taxonomy_cooccurrence_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.taxonomy_cooccurrence instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,148 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
Suite directe ``picarones/core/taxonomy_intra_doc.py``. Pattern
|
| 6 |
-
identique aux autres rendus (Sprints 41/43/62/67/72/74/75) :
|
| 7 |
-
**server-side**, pas de JavaScript, anti-injection systématique.
|
| 8 |
-
|
| 9 |
-
Sortie typique
|
| 10 |
-
--------------
|
| 11 |
-
Une grille N_classes × N_bins où chaque cellule indique la densité
|
| 12 |
-
d'erreurs de cette classe à cette position dans le document.
|
| 13 |
-
Lecture immédiate : « ligature_error concentré dans la première
|
| 14 |
-
tranche → erreur de marge ; visual_confusion uniformément réparti
|
| 15 |
-
→ erreur de scribe ».
|
| 16 |
-
|
| 17 |
-
Adaptive : si ``data is None`` ou si toutes les classes ont 0
|
| 18 |
-
erreur, retourne ``""``.
|
| 19 |
"""
|
| 20 |
|
| 21 |
from __future__ import annotations
|
| 22 |
|
| 23 |
-
|
| 24 |
-
from typing import Optional
|
| 25 |
-
|
| 26 |
-
from picarones.reports_v2._helpers.render_helpers import (
|
| 27 |
-
GRADIENT_TARGET_ORANGE,
|
| 28 |
-
build_grid_svg,
|
| 29 |
-
color_single_gradient,
|
| 30 |
-
text_color_for_bg,
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def _build_position_heatmap_svg(
|
| 35 |
-
classes_with_errors: list[str],
|
| 36 |
-
per_class: dict[str, list[int]],
|
| 37 |
-
n_bins: int,
|
| 38 |
-
*,
|
| 39 |
-
cell_w: int = 36,
|
| 40 |
-
cell_h: int = 26,
|
| 41 |
-
label_left: int = 150,
|
| 42 |
-
label_top: int = 30,
|
| 43 |
-
) -> str:
|
| 44 |
-
"""Heatmap class taxonomique × position (densité relative par classe).
|
| 45 |
-
|
| 46 |
-
Délègue à :func:`build_grid_svg` ; reste un wrapper local qui
|
| 47 |
-
encapsule la normalisation par classe (densité relative au max
|
| 48 |
-
observé sur la ligne).
|
| 49 |
-
"""
|
| 50 |
-
if not classes_with_errors:
|
| 51 |
-
return ""
|
| 52 |
-
|
| 53 |
-
# Pré-calcule densité et count par cellule pour éviter les boucles
|
| 54 |
-
# imbriquées dans les callbacks.
|
| 55 |
-
grid: list[list[tuple[int, float]]] = []
|
| 56 |
-
for cls in classes_with_errors:
|
| 57 |
-
counts = per_class.get(cls, [0] * n_bins)
|
| 58 |
-
max_count = max(counts) if counts else 0
|
| 59 |
-
row: list[tuple[int, float]] = []
|
| 60 |
-
for j in range(n_bins):
|
| 61 |
-
count = counts[j] if j < len(counts) else 0
|
| 62 |
-
density = (count / max_count) if max_count > 0 else 0.0
|
| 63 |
-
row.append((count, density))
|
| 64 |
-
grid.append(row)
|
| 65 |
|
| 66 |
-
|
| 67 |
-
n_rows=len(classes_with_errors),
|
| 68 |
-
n_cols=n_bins,
|
| 69 |
-
row_label_fn=lambda i: classes_with_errors[i],
|
| 70 |
-
col_label_fn=lambda j: str(j + 1),
|
| 71 |
-
cell_color_fn=lambda i, j: color_single_gradient(
|
| 72 |
-
grid[i][j][1], end_rgb=GRADIENT_TARGET_ORANGE,
|
| 73 |
-
),
|
| 74 |
-
cell_text_fn=lambda i, j: (
|
| 75 |
-
str(grid[i][j][0]) if grid[i][j][0] > 0 else None
|
| 76 |
-
),
|
| 77 |
-
cell_text_color_fn=lambda i, j: text_color_for_bg(grid[i][j][1]),
|
| 78 |
-
cell_w=cell_w,
|
| 79 |
-
cell_h=cell_h,
|
| 80 |
-
label_left=label_left,
|
| 81 |
-
label_top=label_top,
|
| 82 |
-
aria_label="Heatmap class taxonomique × position",
|
| 83 |
-
x_axis_title="Position dans le document (1 = début)",
|
| 84 |
-
)
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
Retourne ``""`` si ``data is None`` ou aucune erreur.
|
| 94 |
-
"""
|
| 95 |
-
if not data:
|
| 96 |
-
return ""
|
| 97 |
-
n_bins = data.get("n_bins", 0)
|
| 98 |
-
per_class = data.get("per_class") or {}
|
| 99 |
-
total_errors = data.get("total_errors", 0)
|
| 100 |
-
if total_errors == 0 or n_bins <= 0:
|
| 101 |
-
return ""
|
| 102 |
-
# Filtre : uniquement les classes ayant au moins une erreur
|
| 103 |
-
classes_with_errors = [
|
| 104 |
-
cls for cls, counts in per_class.items()
|
| 105 |
-
if isinstance(counts, list) and sum(counts) > 0
|
| 106 |
-
]
|
| 107 |
-
if not classes_with_errors:
|
| 108 |
-
return ""
|
| 109 |
-
|
| 110 |
-
labels = labels or {}
|
| 111 |
-
title = labels.get(
|
| 112 |
-
"intradoc_title",
|
| 113 |
-
"Évolution intra-document des classes d'erreur",
|
| 114 |
-
)
|
| 115 |
-
note = labels.get(
|
| 116 |
-
"intradoc_note",
|
| 117 |
-
"Heatmap class × position : densité relative par classe "
|
| 118 |
-
"(plus foncé = concentré). Une classe concentrée dans la "
|
| 119 |
-
"première colonne suggère une erreur de marge ; "
|
| 120 |
-
"une distribution uniforme suggère une erreur de scribe.",
|
| 121 |
-
)
|
| 122 |
-
n_words_gt = data.get("n_words_gt", 0)
|
| 123 |
-
n_words_template = labels.get(
|
| 124 |
-
"intradoc_n_words",
|
| 125 |
-
"Calculé sur {n_words_gt} mots GT, répartis en {n_bins} tranches.",
|
| 126 |
-
)
|
| 127 |
-
n_words_phrase = n_words_template.format(
|
| 128 |
-
n_words_gt=n_words_gt, n_bins=n_bins,
|
| 129 |
-
)
|
| 130 |
-
|
| 131 |
-
svg = _build_position_heatmap_svg(classes_with_errors, per_class, n_bins)
|
| 132 |
-
|
| 133 |
-
parts = [
|
| 134 |
-
'<div class="intradoc" style="margin:1rem 0">',
|
| 135 |
-
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 136 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 137 |
-
f'{_e(note)}</div>',
|
| 138 |
-
f'<div style="font-size:.8rem;opacity:.7;margin-bottom:.5rem">'
|
| 139 |
-
f'{_e(n_words_phrase)}</div>',
|
| 140 |
-
svg,
|
| 141 |
-
"</div>",
|
| 142 |
-
]
|
| 143 |
-
return "".join(parts)
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
__all__ = [
|
| 147 |
-
"build_taxonomy_intra_doc_html",
|
| 148 |
-
]
|
|
|
|
| 1 |
+
"""``picarones.report.taxonomy_intra_doc_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.taxonomy_intra_doc`.
|
| 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.taxonomy_intra_doc import * # noqa: F401, F403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.taxonomy_intra_doc_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.taxonomy_intra_doc instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -136,7 +136,7 @@ def build_advanced_taxonomy_view_html(
|
|
| 136 |
pair = _select_two_engines_for_comparison(engines_summary)
|
| 137 |
if pair is not None:
|
| 138 |
from picarones.measurements.taxonomy_comparison import compare_taxonomies
|
| 139 |
-
from picarones.
|
| 140 |
build_taxonomy_comparison_html,
|
| 141 |
)
|
| 142 |
engine_a, engine_b = pair
|
|
@@ -163,7 +163,7 @@ def build_advanced_taxonomy_view_html(
|
|
| 163 |
# Sous-section 2 : co-occurrence (opt-in)
|
| 164 |
if cooccurrence:
|
| 165 |
try:
|
| 166 |
-
from picarones.
|
| 167 |
build_taxonomy_cooccurrence_html,
|
| 168 |
)
|
| 169 |
html = build_taxonomy_cooccurrence_html(cooccurrence, labels=labels)
|
|
@@ -183,7 +183,7 @@ def build_advanced_taxonomy_view_html(
|
|
| 183 |
# Sous-section 3 : intra-document (opt-in)
|
| 184 |
if intra_doc:
|
| 185 |
try:
|
| 186 |
-
from picarones.
|
| 187 |
build_taxonomy_intra_doc_html,
|
| 188 |
)
|
| 189 |
html = build_taxonomy_intra_doc_html(intra_doc, labels=labels)
|
|
|
|
| 136 |
pair = _select_two_engines_for_comparison(engines_summary)
|
| 137 |
if pair is not None:
|
| 138 |
from picarones.measurements.taxonomy_comparison import compare_taxonomies
|
| 139 |
+
from picarones.reports_v2.html.renderers.taxonomy_comparison import (
|
| 140 |
build_taxonomy_comparison_html,
|
| 141 |
)
|
| 142 |
engine_a, engine_b = pair
|
|
|
|
| 163 |
# Sous-section 2 : co-occurrence (opt-in)
|
| 164 |
if cooccurrence:
|
| 165 |
try:
|
| 166 |
+
from picarones.reports_v2.html.renderers.taxonomy_cooccurrence import (
|
| 167 |
build_taxonomy_cooccurrence_html,
|
| 168 |
)
|
| 169 |
html = build_taxonomy_cooccurrence_html(cooccurrence, labels=labels)
|
|
|
|
| 183 |
# Sous-section 3 : intra-document (opt-in)
|
| 184 |
if intra_doc:
|
| 185 |
try:
|
| 186 |
+
from picarones.reports_v2.html.renderers.taxonomy_intra_doc import (
|
| 187 |
build_taxonomy_intra_doc_html,
|
| 188 |
)
|
| 189 |
html = build_taxonomy_intra_doc_html(intra_doc, labels=labels)
|
|
@@ -206,7 +206,7 @@ def build_diagnostics_view_html(
|
|
| 206 |
if benchmark is not None:
|
| 207 |
try:
|
| 208 |
from picarones.measurements.worst_lines import extract_worst_lines
|
| 209 |
-
from picarones.
|
| 210 |
build_worst_lines_table_html,
|
| 211 |
)
|
| 212 |
entries = extract_worst_lines(benchmark, top_n=20)
|
|
|
|
| 206 |
if benchmark is not None:
|
| 207 |
try:
|
| 208 |
from picarones.measurements.worst_lines import extract_worst_lines
|
| 209 |
+
from picarones.reports_v2.html.renderers.worst_lines import (
|
| 210 |
build_worst_lines_table_html,
|
| 211 |
)
|
| 212 |
entries = extract_worst_lines(benchmark, top_n=20)
|
|
@@ -124,7 +124,7 @@ def build_pipeline_view_html(
|
|
| 124 |
# Sous-section 2 : DAG visualization
|
| 125 |
if dag_nodes:
|
| 126 |
try:
|
| 127 |
-
from picarones.
|
| 128 |
build_pipeline_dag_html,
|
| 129 |
)
|
| 130 |
html = build_pipeline_dag_html(
|
|
|
|
| 124 |
# Sous-section 2 : DAG visualization
|
| 125 |
if dag_nodes:
|
| 126 |
try:
|
| 127 |
+
from picarones.reports_v2.html.renderers.pipeline_dag import (
|
| 128 |
build_pipeline_dag_html,
|
| 129 |
)
|
| 130 |
html = build_pipeline_dag_html(
|
|
@@ -1,164 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
**server-side**, pas de JavaScript, anti-injection systématique
|
| 6 |
-
via ``html.escape``.
|
| 7 |
-
|
| 8 |
-
Vue distincte du tableau gallery existant
|
| 9 |
-
-----------------------------------------
|
| 10 |
-
La galerie OCR (vue ``view_gallery.html``) liste les documents
|
| 11 |
-
les plus problématiques. Cette vue va plus fin : elle liste les
|
| 12 |
-
**lignes individuelles** les plus problématiques, transversalement
|
| 13 |
-
à tous les documents et moteurs. Complémentaire, pas redondante.
|
| 14 |
"""
|
| 15 |
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
-
|
| 19 |
-
from typing import Optional
|
| 20 |
-
|
| 21 |
-
from picarones.measurements.worst_lines import WorstLineEntry
|
| 22 |
-
from picarones.core.diff_utils import compute_char_diff
|
| 23 |
-
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def _bg_for_cer(cer: float) -> str:
|
| 27 |
-
"""Beige clair sous le seuil catastrophique (0.30), gradient
|
| 28 |
-
jaune → rouge au-delà.
|
| 29 |
-
|
| 30 |
-
Le seuil dur à 0.30 préserve la sémantique « toléré jusqu'à 30 %
|
| 31 |
-
pour un manuscrit difficile ». Au-delà, on entre en zone visible
|
| 32 |
-
avec :func:`color_traffic_light` (low_is_good).
|
| 33 |
-
"""
|
| 34 |
-
f = max(0.0, min(1.0, cer))
|
| 35 |
-
if f < 0.3:
|
| 36 |
-
return "#fff8dc"
|
| 37 |
-
return color_traffic_light(f, low_is_good=True, scale_min=0.3, scale_max=1.0)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def _render_diff_inline(reference: str, hypothesis: str) -> str:
|
| 41 |
-
"""Rendu HTML inline d'un diff caractère par caractère.
|
| 42 |
-
|
| 43 |
-
- ``equal`` → texte normal
|
| 44 |
-
- ``delete`` → fond rouge clair, barré (manquait dans hyp)
|
| 45 |
-
- ``insert`` → fond vert clair (ajouté par hyp)
|
| 46 |
-
- ``replace`` → fond rouge clair barré + fond vert clair pour
|
| 47 |
-
la nouvelle valeur (côte à côte)
|
| 48 |
-
"""
|
| 49 |
-
if not reference and not hypothesis:
|
| 50 |
-
return '<span style="opacity:.5">∅</span>'
|
| 51 |
-
ops = compute_char_diff(reference or "", hypothesis or "")
|
| 52 |
-
parts: list[str] = []
|
| 53 |
-
for op in ops:
|
| 54 |
-
kind = op["op"]
|
| 55 |
-
if kind == "equal":
|
| 56 |
-
parts.append(_e(op["text"]))
|
| 57 |
-
elif kind == "delete":
|
| 58 |
-
parts.append(
|
| 59 |
-
f'<span style="background:#fdd;text-decoration:line-through">'
|
| 60 |
-
f'{_e(op["text"])}</span>'
|
| 61 |
-
)
|
| 62 |
-
elif kind == "insert":
|
| 63 |
-
parts.append(
|
| 64 |
-
f'<span style="background:#dfd">{_e(op["text"])}</span>'
|
| 65 |
-
)
|
| 66 |
-
elif kind == "replace":
|
| 67 |
-
parts.append(
|
| 68 |
-
f'<span style="background:#fdd;text-decoration:line-through">'
|
| 69 |
-
f'{_e(op["old"])}</span>'
|
| 70 |
-
f'<span style="background:#dfd">{_e(op["new"])}</span>'
|
| 71 |
-
)
|
| 72 |
-
return "".join(parts)
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def build_worst_lines_table_html(
|
| 76 |
-
entries: list[WorstLineEntry],
|
| 77 |
-
labels: Optional[dict[str, str]] = None,
|
| 78 |
-
) -> str:
|
| 79 |
-
"""Construit le tableau HTML des worst lines.
|
| 80 |
-
|
| 81 |
-
Retourne ``""`` si la liste est vide. Adaptive : si aucune
|
| 82 |
-
entrée n'a de ``script_type``, la colonne strate est omise.
|
| 83 |
-
"""
|
| 84 |
-
if not entries:
|
| 85 |
-
return ""
|
| 86 |
-
labels = labels or {}
|
| 87 |
-
title = labels.get("worst_lines_title", "Lignes les plus problématiques")
|
| 88 |
-
note = labels.get(
|
| 89 |
-
"worst_lines_note",
|
| 90 |
-
"Top-N lignes du corpus classées par CER ligne décroissant. "
|
| 91 |
-
"Diff caractère par caractère : rouge barré = manquant dans "
|
| 92 |
-
"l'OCR, vert = ajouté par l'OCR.",
|
| 93 |
-
)
|
| 94 |
-
rank_label = labels.get("worst_lines_rank_label", "Rang")
|
| 95 |
-
cer_label = labels.get("worst_lines_cer_label", "CER")
|
| 96 |
-
engine_label = labels.get("worst_lines_engine_label", "Moteur")
|
| 97 |
-
doc_label = labels.get("worst_lines_doc_label", "Document")
|
| 98 |
-
line_label = labels.get("worst_lines_line_label", "Ligne #")
|
| 99 |
-
strata_label = labels.get("worst_lines_strata_label", "Strate")
|
| 100 |
-
diff_label = labels.get("worst_lines_diff_label", "GT → OCR (diff)")
|
| 101 |
-
|
| 102 |
-
has_strata = any(e.script_type for e in entries)
|
| 103 |
-
|
| 104 |
-
parts = [
|
| 105 |
-
'<div class="worst-lines" style="margin:1rem 0">',
|
| 106 |
-
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 107 |
-
f'<div style="font-size:.8rem;opacity:.75;margin-bottom:.5rem">'
|
| 108 |
-
f'{_e(note)}</div>',
|
| 109 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 110 |
-
'font-size:.85rem">',
|
| 111 |
-
'<thead><tr>',
|
| 112 |
-
]
|
| 113 |
-
cols = [rank_label, cer_label, engine_label, doc_label, line_label]
|
| 114 |
-
if has_strata:
|
| 115 |
-
cols.append(strata_label)
|
| 116 |
-
cols.append(diff_label)
|
| 117 |
-
for col in cols:
|
| 118 |
-
parts.append(
|
| 119 |
-
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 120 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 121 |
-
f'{_e(col)}</th>'
|
| 122 |
-
)
|
| 123 |
-
parts.append("</tr></thead><tbody>")
|
| 124 |
-
for entry in entries:
|
| 125 |
-
cer_color = _bg_for_cer(entry.cer)
|
| 126 |
-
parts.append("<tr>")
|
| 127 |
-
parts.append(
|
| 128 |
-
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 129 |
-
f'font-weight:600">{entry.rank}</td>'
|
| 130 |
-
)
|
| 131 |
-
parts.append(
|
| 132 |
-
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 133 |
-
f'background:{cer_color};font-family:monospace">'
|
| 134 |
-
f'{entry.cer * 100:.1f}%</td>'
|
| 135 |
-
)
|
| 136 |
-
parts.append(
|
| 137 |
-
f'<td style="padding:.3rem .5rem">{_e(entry.engine_name)}</td>'
|
| 138 |
-
)
|
| 139 |
-
parts.append(
|
| 140 |
-
f'<td style="padding:.3rem .5rem;font-family:monospace;'
|
| 141 |
-
f'font-size:.8rem">{_e(entry.doc_id)}</td>'
|
| 142 |
-
)
|
| 143 |
-
parts.append(
|
| 144 |
-
f'<td style="padding:.3rem .5rem;text-align:right">'
|
| 145 |
-
f'{entry.line_index}</td>'
|
| 146 |
-
)
|
| 147 |
-
if has_strata:
|
| 148 |
-
parts.append(
|
| 149 |
-
f'<td style="padding:.3rem .5rem;font-size:.8rem">'
|
| 150 |
-
f'{_e(entry.script_type or "—")}</td>'
|
| 151 |
-
)
|
| 152 |
-
parts.append(
|
| 153 |
-
f'<td style="padding:.3rem .5rem;font-family:monospace;'
|
| 154 |
-
f'font-size:.85rem">'
|
| 155 |
-
f'{_render_diff_inline(entry.gt_line, entry.hyp_line)}</td>'
|
| 156 |
-
)
|
| 157 |
-
parts.append("</tr>")
|
| 158 |
-
parts.append("</tbody></table></div>")
|
| 159 |
-
return "".join(parts)
|
| 160 |
|
|
|
|
| 161 |
|
| 162 |
-
|
| 163 |
-
"
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.worst_lines_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.worst_lines`.
|
| 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.worst_lines import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.worst_lines_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.worst_lines instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Visualisation DAG d'un pipeline composé — Sprint 95 (B.4).
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.pipeline_dag_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.pipeline_dag``. Le chemin
|
| 6 |
+
legacy reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 7 |
+
suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Sprint 95 — B.4 du plan d'évolution 2026.
|
| 10 |
+
|
| 11 |
+
Outil d'inspection, pas de construction
|
| 12 |
+
---------------------------------------
|
| 13 |
+
Le YAML reste source de vérité. Cette vue **affiche** le
|
| 14 |
+
graphe orienté de la pipeline pour permettre l'inspection et
|
| 15 |
+
le debug d'un benchmark d'axe B (Sprint 63+) — elle ne
|
| 16 |
+
construit rien, ne supporte pas le drag-and-drop, n'exporte
|
| 17 |
+
aucun JSON modifiable.
|
| 18 |
+
|
| 19 |
+
Pattern identique aux autres rendus : SVG **server-side**,
|
| 20 |
+
pas de JS, anti-injection systématique.
|
| 21 |
+
|
| 22 |
+
Vue
|
| 23 |
+
---
|
| 24 |
+
Layout horizontal de gauche à droite :
|
| 25 |
+
|
| 26 |
+
- Chaque **nœud** est un rectangle annoté du nom du module et
|
| 27 |
+
de ses types d'entrée/sortie.
|
| 28 |
+
- Chaque **arête** porte une étiquette : type d'artefact +
|
| 29 |
+
métrique principale + valeur, avec un code couleur
|
| 30 |
+
vert/jaune/rouge selon le seuil sur la valeur.
|
| 31 |
+
|
| 32 |
+
Adaptive : ``""`` si moins d'un nœud.
|
| 33 |
+
|
| 34 |
+
Note d'intégration
|
| 35 |
+
------------------
|
| 36 |
+
Module pur — l'utilisateur compose les structures simples
|
| 37 |
+
``nodes`` et ``edges`` depuis sa ``PipelineSpec`` (Sprint 63)
|
| 38 |
+
et son ``PipelineBenchmarkResult`` (Sprint 64) :
|
| 39 |
+
|
| 40 |
+
.. code-block:: python
|
| 41 |
+
|
| 42 |
+
from picarones.reports_v2.html.renderers.pipeline_dag import build_pipeline_dag_html
|
| 43 |
+
|
| 44 |
+
nodes = [
|
| 45 |
+
{"name": s.name, "input_types": [t.value for t in s.module.input_types],
|
| 46 |
+
"output_types": [t.value for t in s.module.output_types]}
|
| 47 |
+
for s in spec.steps
|
| 48 |
+
]
|
| 49 |
+
edges = []
|
| 50 |
+
for prev, curr in zip(spec.steps, spec.steps[1:]):
|
| 51 |
+
agg = bench.aggregate_for_step(curr.name)
|
| 52 |
+
for art_type, metrics in (agg.junction_metrics or {}).items():
|
| 53 |
+
for metric_name, value in metrics.items():
|
| 54 |
+
edges.append({
|
| 55 |
+
"from": prev.name, "to": curr.name,
|
| 56 |
+
"artifact_type": art_type, "metric_name": metric_name,
|
| 57 |
+
"metric_value": value.get("mean"),
|
| 58 |
+
})
|
| 59 |
+
html = build_pipeline_dag_html(nodes, edges, labels)
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
from __future__ import annotations
|
| 63 |
+
|
| 64 |
+
from html import escape as _e
|
| 65 |
+
from typing import Optional
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# Seuils par défaut sur les métriques d'erreur (CER-like, lower is better).
|
| 69 |
+
_DEFAULT_THRESHOLDS = (0.05, 0.15) # vert ≤ 0.05, jaune ≤ 0.15, rouge > 0.15
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _classify_metric(
|
| 73 |
+
value: Optional[float],
|
| 74 |
+
thresholds: tuple[float, float],
|
| 75 |
+
higher_is_better: bool,
|
| 76 |
+
) -> str:
|
| 77 |
+
"""Retourne ``"green"``, ``"yellow"``, ``"red"`` ou ``"none"``."""
|
| 78 |
+
if value is None:
|
| 79 |
+
return "none"
|
| 80 |
+
try:
|
| 81 |
+
v = float(value)
|
| 82 |
+
except (TypeError, ValueError):
|
| 83 |
+
return "none"
|
| 84 |
+
low, high = thresholds
|
| 85 |
+
if higher_is_better:
|
| 86 |
+
# Inversion : haut = bon
|
| 87 |
+
if v >= 1.0 - low:
|
| 88 |
+
return "green"
|
| 89 |
+
if v >= 1.0 - high:
|
| 90 |
+
return "yellow"
|
| 91 |
+
return "red"
|
| 92 |
+
if v <= low:
|
| 93 |
+
return "green"
|
| 94 |
+
if v <= high:
|
| 95 |
+
return "yellow"
|
| 96 |
+
return "red"
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# Sprint A7 (m-5) — palette Okabe-Ito daltonien-friendly importée
|
| 100 |
+
# depuis le module canonique ``picarones.report.colors``. Avant
|
| 101 |
+
# A7, les hex étaient hardcodés (rouge/vert classiques, problème
|
| 102 |
+
# pour la deutéranopie) ; maintenant cohérent avec _cer_color et
|
| 103 |
+
# difficulty_color.
|
| 104 |
+
from picarones.reports_v2._helpers.colors import COLOR_GREEN, COLOR_RED, COLOR_YELLOW
|
| 105 |
+
|
| 106 |
+
_QUALITY_COLORS = {
|
| 107 |
+
"green": COLOR_GREEN, # Okabe-Ito blue (substitut sémantique « bon »)
|
| 108 |
+
"yellow": COLOR_YELLOW, # Okabe-Ito yellow
|
| 109 |
+
"red": COLOR_RED, # Okabe-Ito vermillion (substitut sémantique « mauvais »)
|
| 110 |
+
"none": "#6b7280",
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _format_value(value: Optional[float]) -> str:
|
| 115 |
+
if value is None:
|
| 116 |
+
return "—"
|
| 117 |
+
try:
|
| 118 |
+
v = float(value)
|
| 119 |
+
except (TypeError, ValueError):
|
| 120 |
+
return "—"
|
| 121 |
+
if abs(v) < 1.0:
|
| 122 |
+
return f"{v * 100:.1f}%"
|
| 123 |
+
return f"{v:.2f}"
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def build_pipeline_dag_html(
|
| 127 |
+
nodes: Optional[list[dict]],
|
| 128 |
+
labels: Optional[dict[str, str]] = None,
|
| 129 |
+
edges: Optional[list[dict]] = None,
|
| 130 |
+
*,
|
| 131 |
+
thresholds: tuple[float, float] = _DEFAULT_THRESHOLDS,
|
| 132 |
+
higher_is_better: bool = False,
|
| 133 |
+
) -> str:
|
| 134 |
+
"""Construit la vue HTML « Pipeline DAG ».
|
| 135 |
+
|
| 136 |
+
Parameters
|
| 137 |
+
----------
|
| 138 |
+
nodes:
|
| 139 |
+
Liste de dicts ``{"name", "input_types"?, "output_types"?}``
|
| 140 |
+
dans l'ordre topologique. Si vide ou ``None``, retourne
|
| 141 |
+
``""``.
|
| 142 |
+
labels:
|
| 143 |
+
Dict i18n. Clés sous le préfixe ``dag_*``.
|
| 144 |
+
edges:
|
| 145 |
+
Liste de dicts ``{"from", "to", "artifact_type"?,
|
| 146 |
+
"metric_name"?, "metric_value"?}``. Optionnel —
|
| 147 |
+
auto-déduit séquentiel sinon.
|
| 148 |
+
thresholds:
|
| 149 |
+
``(seuil_vert, seuil_jaune)`` sur la valeur de métrique.
|
| 150 |
+
Défaut ``(0.05, 0.15)`` — convention CER.
|
| 151 |
+
higher_is_better:
|
| 152 |
+
Si ``True``, la sémantique est inversée (1 = meilleur).
|
| 153 |
+
"""
|
| 154 |
+
nodes = list(nodes or [])
|
| 155 |
+
if not nodes:
|
| 156 |
+
return ""
|
| 157 |
+
edges = list(edges or [])
|
| 158 |
+
labels = labels or {}
|
| 159 |
+
title = labels.get("dag_title", "Pipeline DAG")
|
| 160 |
+
note = labels.get(
|
| 161 |
+
"dag_note",
|
| 162 |
+
"Graphe orienté du pipeline composé. Chaque arête porte "
|
| 163 |
+
"le type d'artefact transmis et la métrique calculée à "
|
| 164 |
+
"la jonction. Code couleur vert/orange/rouge selon le "
|
| 165 |
+
"seuil. Outil d'inspection — le YAML reste source de "
|
| 166 |
+
"vérité.",
|
| 167 |
+
)
|
| 168 |
+
# Layout horizontal régulier
|
| 169 |
+
n = len(nodes)
|
| 170 |
+
box_width = 160
|
| 171 |
+
box_height = 70
|
| 172 |
+
h_gap = 110 # espace horizontal entre nœuds
|
| 173 |
+
margin = 30
|
| 174 |
+
svg_width = margin * 2 + n * box_width + (n - 1) * h_gap
|
| 175 |
+
svg_height = box_height + margin * 2 + 60 # +60 pour étiquettes arêtes
|
| 176 |
+
centre_y = margin + box_height / 2 + 30 # offset pour étiquette de tête
|
| 177 |
+
|
| 178 |
+
# Index des nœuds par name pour récupérer la position
|
| 179 |
+
node_x: dict[str, float] = {}
|
| 180 |
+
parts: list[str] = [
|
| 181 |
+
'<section class="dag-section" style="margin:1rem 0">',
|
| 182 |
+
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 183 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 184 |
+
f'{_e(note)}</div>',
|
| 185 |
+
f'<svg viewBox="0 0 {svg_width} {svg_height}" '
|
| 186 |
+
f'role="img" aria-label="{_e(title)}" '
|
| 187 |
+
'xmlns="http://www.w3.org/2000/svg" '
|
| 188 |
+
'style="max-width:100%;height:auto;'
|
| 189 |
+
'font-family:system-ui,sans-serif;font-size:12px">',
|
| 190 |
+
# Définition d'une flèche
|
| 191 |
+
'<defs>'
|
| 192 |
+
'<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" '
|
| 193 |
+
'markerWidth="6" markerHeight="6" orient="auto-start-reverse">'
|
| 194 |
+
'<path d="M0,0 L10,5 L0,10 z" fill="#374151"/>'
|
| 195 |
+
'</marker>'
|
| 196 |
+
'</defs>',
|
| 197 |
+
]
|
| 198 |
+
|
| 199 |
+
# Étape 1 : nœuds
|
| 200 |
+
for i, node in enumerate(nodes):
|
| 201 |
+
name = str(node.get("name") or f"step_{i}")
|
| 202 |
+
x = margin + i * (box_width + h_gap)
|
| 203 |
+
y = margin + 30
|
| 204 |
+
node_x[name] = x + box_width
|
| 205 |
+
in_types = ", ".join(node.get("input_types") or [])
|
| 206 |
+
out_types = ", ".join(node.get("output_types") or [])
|
| 207 |
+
parts.append(
|
| 208 |
+
f'<rect x="{x}" y="{y}" width="{box_width}" '
|
| 209 |
+
f'height="{box_height}" rx="6" fill="#f3f4f6" '
|
| 210 |
+
f'stroke="#374151" stroke-width="1.5"/>'
|
| 211 |
+
)
|
| 212 |
+
parts.append(
|
| 213 |
+
f'<text x="{x + box_width / 2}" y="{y + 22}" '
|
| 214 |
+
f'text-anchor="middle" font-weight="600" '
|
| 215 |
+
f'fill="#111827">{_e(name)}</text>'
|
| 216 |
+
)
|
| 217 |
+
if in_types:
|
| 218 |
+
parts.append(
|
| 219 |
+
f'<text x="{x + box_width / 2}" y="{y + 40}" '
|
| 220 |
+
f'text-anchor="middle" fill="#4b5563" '
|
| 221 |
+
f'font-size="10">in: {_e(in_types)}</text>'
|
| 222 |
+
)
|
| 223 |
+
if out_types:
|
| 224 |
+
parts.append(
|
| 225 |
+
f'<text x="{x + box_width / 2}" y="{y + 56}" '
|
| 226 |
+
f'text-anchor="middle" fill="#4b5563" '
|
| 227 |
+
f'font-size="10">out: {_e(out_types)}</text>'
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Étape 2 : arêtes (mappées sur paires séquentielles si pas de
|
| 231 |
+
# "from"/"to" explicites — voir nodes par défaut)
|
| 232 |
+
auto_edges: list[dict] = []
|
| 233 |
+
if not edges:
|
| 234 |
+
for prev, curr in zip(nodes, nodes[1:]):
|
| 235 |
+
auto_edges.append({
|
| 236 |
+
"from": prev.get("name"),
|
| 237 |
+
"to": curr.get("name"),
|
| 238 |
+
})
|
| 239 |
+
else:
|
| 240 |
+
auto_edges = edges
|
| 241 |
+
|
| 242 |
+
for edge in auto_edges:
|
| 243 |
+
src = str(edge.get("from") or "")
|
| 244 |
+
dst = str(edge.get("to") or "")
|
| 245 |
+
if not src or not dst:
|
| 246 |
+
continue
|
| 247 |
+
# Position : du bord droit du src au bord gauche du dst
|
| 248 |
+
# Heuristique : on prend la position du nœud src dans la
|
| 249 |
+
# liste pour calculer x1, et celle de dst pour x2.
|
| 250 |
+
try:
|
| 251 |
+
i_src = next(
|
| 252 |
+
i for i, n_ in enumerate(nodes)
|
| 253 |
+
if n_.get("name") == src
|
| 254 |
+
)
|
| 255 |
+
i_dst = next(
|
| 256 |
+
i for i, n_ in enumerate(nodes)
|
| 257 |
+
if n_.get("name") == dst
|
| 258 |
+
)
|
| 259 |
+
except StopIteration:
|
| 260 |
+
continue
|
| 261 |
+
x1 = margin + i_src * (box_width + h_gap) + box_width
|
| 262 |
+
x2 = margin + i_dst * (box_width + h_gap)
|
| 263 |
+
y = centre_y
|
| 264 |
+
# Classe la métrique pour le code couleur
|
| 265 |
+
value = edge.get("metric_value")
|
| 266 |
+
try:
|
| 267 |
+
value_f = float(value) if value is not None else None
|
| 268 |
+
except (TypeError, ValueError):
|
| 269 |
+
value_f = None
|
| 270 |
+
cls = _classify_metric(value_f, thresholds, higher_is_better)
|
| 271 |
+
color = _QUALITY_COLORS[cls]
|
| 272 |
+
# Trace la flèche
|
| 273 |
+
parts.append(
|
| 274 |
+
f'<line x1="{x1}" y1="{y}" x2="{x2}" y2="{y}" '
|
| 275 |
+
f'stroke="{color}" stroke-width="2" '
|
| 276 |
+
f'marker-end="url(#arrow)"/>'
|
| 277 |
+
)
|
| 278 |
+
# Étiquette : type + métrique : valeur
|
| 279 |
+
artifact_type = edge.get("artifact_type") or ""
|
| 280 |
+
metric_name = edge.get("metric_name") or ""
|
| 281 |
+
value_str = _format_value(value_f)
|
| 282 |
+
label_lines: list[str] = []
|
| 283 |
+
if artifact_type:
|
| 284 |
+
label_lines.append(str(artifact_type))
|
| 285 |
+
if metric_name:
|
| 286 |
+
label_lines.append(f"{metric_name}: {value_str}")
|
| 287 |
+
if label_lines:
|
| 288 |
+
label_x = (x1 + x2) / 2
|
| 289 |
+
for k, line in enumerate(label_lines):
|
| 290 |
+
parts.append(
|
| 291 |
+
f'<text x="{label_x}" y="{y - 8 - k * 12}" '
|
| 292 |
+
f'text-anchor="middle" fill="{color}" '
|
| 293 |
+
f'font-size="10" font-weight="600">'
|
| 294 |
+
f'{_e(line)}</text>'
|
| 295 |
+
)
|
| 296 |
+
parts.append("</svg>")
|
| 297 |
+
|
| 298 |
+
# Légende
|
| 299 |
+
h_legend = labels.get("dag_legend", "Lecture")
|
| 300 |
+
legend_green = labels.get("dag_legend_green", "qualité élevée")
|
| 301 |
+
legend_yellow = labels.get("dag_legend_yellow", "qualité moyenne")
|
| 302 |
+
legend_red = labels.get("dag_legend_red", "qualité faible")
|
| 303 |
+
parts.append(
|
| 304 |
+
'<div style="font-size:.8rem;opacity:.75;margin-top:.4rem">'
|
| 305 |
+
f'<strong>{_e(h_legend)} :</strong> '
|
| 306 |
+
f'<span style="color:{_QUALITY_COLORS["green"]};'
|
| 307 |
+
f'font-weight:600">●</span> {_e(legend_green)} '
|
| 308 |
+
f'(≤ {thresholds[0] * 100:.0f}%) '
|
| 309 |
+
f'<span style="color:{_QUALITY_COLORS["yellow"]};'
|
| 310 |
+
f'font-weight:600">●</span> {_e(legend_yellow)} '
|
| 311 |
+
f'(≤ {thresholds[1] * 100:.0f}%) '
|
| 312 |
+
f'<span style="color:{_QUALITY_COLORS["red"]};'
|
| 313 |
+
f'font-weight:600">●</span> {_e(legend_red)}'
|
| 314 |
+
'</div>'
|
| 315 |
+
)
|
| 316 |
+
parts.append("</section>")
|
| 317 |
+
return "".join(parts)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
__all__ = ["build_pipeline_dag_html"]
|
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML du diagramme miroir taxonomique — Sprint 77.
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.taxonomy_comparison_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.taxonomy_comparison``.
|
| 6 |
+
Le chemin legacy reste disponible via un shim avec
|
| 7 |
+
``DeprecationWarning`` ; suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
A.I.4 chantier 3 du plan d'évolution 2026.
|
| 10 |
+
|
| 11 |
+
Suite directe ``picarones/core/taxonomy_comparison.py``. Pattern
|
| 12 |
+
identique aux autres rendus (Sprints 41/43/62/67/72/74/75/76) :
|
| 13 |
+
**server-side**, pas de JavaScript, anti-injection systématique.
|
| 14 |
+
|
| 15 |
+
Diagramme miroir
|
| 16 |
+
----------------
|
| 17 |
+
Une ligne par classe taxonomique, divisée en deux barres
|
| 18 |
+
horizontales :
|
| 19 |
+
|
| 20 |
+
- À **gauche** : barre du moteur A (orientée vers la gauche, du
|
| 21 |
+
centre vers le bord).
|
| 22 |
+
- À **droite** : barre du moteur B (orientée vers la droite).
|
| 23 |
+
- Couleur de la classe selon ``recoverability`` :
|
| 24 |
+
|
| 25 |
+
- vert (#5fa860) : ``recoverable``
|
| 26 |
+
- orange (#e0a050) : ``difficult``
|
| 27 |
+
- rouge (#d8553b) : ``irrecoverable``
|
| 28 |
+
|
| 29 |
+
Lecture immédiate : un moteur dont les barres tirent vers la
|
| 30 |
+
**gauche** sur du vert (case_error, ligature_error) et un moteur
|
| 31 |
+
qui tire à droite sur du rouge (lacuna) — la décision éditoriale
|
| 32 |
+
est évidente même si les CER globaux sont identiques.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
from __future__ import annotations
|
| 36 |
+
|
| 37 |
+
from html import escape as _e
|
| 38 |
+
from typing import Optional
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
_RECOVERABILITY_COLORS = {
|
| 42 |
+
"recoverable": "#5fa860",
|
| 43 |
+
"difficult": "#e0a050",
|
| 44 |
+
"irrecoverable": "#d8553b",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _build_mirror_chart_svg(
|
| 49 |
+
data: dict,
|
| 50 |
+
*,
|
| 51 |
+
bar_max_width: int = 200,
|
| 52 |
+
row_height: int = 22,
|
| 53 |
+
label_width: int = 140,
|
| 54 |
+
margin_top: int = 50,
|
| 55 |
+
margin_bottom: int = 20,
|
| 56 |
+
) -> str:
|
| 57 |
+
"""Construit le diagramme miroir SVG."""
|
| 58 |
+
classes = data["classes"]
|
| 59 |
+
prop_a = data["proportions_a"]
|
| 60 |
+
prop_b = data["proportions_b"]
|
| 61 |
+
recov = data["recoverability"]
|
| 62 |
+
engine_a = data["engine_a"]
|
| 63 |
+
engine_b = data["engine_b"]
|
| 64 |
+
|
| 65 |
+
n_rows = len(classes)
|
| 66 |
+
if n_rows == 0:
|
| 67 |
+
return ""
|
| 68 |
+
|
| 69 |
+
# Échelle : on normalise à la valeur max de toutes les
|
| 70 |
+
# proportions (pour que la classe la plus présente atteigne
|
| 71 |
+
# bar_max_width).
|
| 72 |
+
max_prop = max(
|
| 73 |
+
max(prop_a.values(), default=0.0),
|
| 74 |
+
max(prop_b.values(), default=0.0),
|
| 75 |
+
)
|
| 76 |
+
if max_prop <= 0:
|
| 77 |
+
max_prop = 1.0 # évite division par zéro (cas dégénéré)
|
| 78 |
+
|
| 79 |
+
width = label_width + 2 * bar_max_width + 40
|
| 80 |
+
height = margin_top + n_rows * row_height + margin_bottom
|
| 81 |
+
center = width // 2
|
| 82 |
+
|
| 83 |
+
parts = [
|
| 84 |
+
f'<svg xmlns="http://www.w3.org/2000/svg" '
|
| 85 |
+
f'width="{width}" height="{height}" '
|
| 86 |
+
f'viewBox="0 0 {width} {height}" '
|
| 87 |
+
f'role="img" aria-label="Diagramme miroir taxonomique">',
|
| 88 |
+
# En-têtes des deux moteurs
|
| 89 |
+
f'<text x="{center - bar_max_width // 2}" y="20" '
|
| 90 |
+
f'font-size="13" font-weight="600" fill="#333" '
|
| 91 |
+
f'text-anchor="middle">{_e(engine_a)}</text>',
|
| 92 |
+
f'<text x="{center + bar_max_width // 2}" y="20" '
|
| 93 |
+
f'font-size="13" font-weight="600" fill="#333" '
|
| 94 |
+
f'text-anchor="middle">{_e(engine_b)}</text>',
|
| 95 |
+
# Ligne centrale
|
| 96 |
+
f'<line x1="{center}" y1="{margin_top - 4}" '
|
| 97 |
+
f'x2="{center}" y2="{height - margin_bottom + 4}" '
|
| 98 |
+
f'stroke="#999" stroke-width="1"/>',
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
# Barres
|
| 102 |
+
for i, cls in enumerate(classes):
|
| 103 |
+
y = margin_top + i * row_height
|
| 104 |
+
level = recov.get(cls, "difficult")
|
| 105 |
+
color = _RECOVERABILITY_COLORS.get(level, "#888")
|
| 106 |
+
# Étiquette de classe au centre
|
| 107 |
+
parts.append(
|
| 108 |
+
f'<text x="{center}" y="{y + row_height // 2 + 4}" '
|
| 109 |
+
f'font-size="11" fill="#222" text-anchor="middle" '
|
| 110 |
+
f'font-family="monospace">{_e(cls)}</text>'
|
| 111 |
+
)
|
| 112 |
+
# Barre A (gauche)
|
| 113 |
+
a_width = (prop_a.get(cls, 0.0) / max_prop) * bar_max_width
|
| 114 |
+
if a_width > 0:
|
| 115 |
+
x_a = center - label_width // 2 - a_width
|
| 116 |
+
parts.append(
|
| 117 |
+
f'<rect x="{x_a:.1f}" y="{y + 3}" '
|
| 118 |
+
f'width="{a_width:.1f}" height="{row_height - 6}" '
|
| 119 |
+
f'fill="{color}" stroke="#666" stroke-width="0.5" '
|
| 120 |
+
f'opacity="0.85"/>'
|
| 121 |
+
)
|
| 122 |
+
# Valeur en %
|
| 123 |
+
parts.append(
|
| 124 |
+
f'<text x="{x_a - 3:.1f}" y="{y + row_height // 2 + 4}" '
|
| 125 |
+
f'font-size="10" fill="#444" text-anchor="end">'
|
| 126 |
+
f'{prop_a.get(cls, 0.0) * 100:.1f}%</text>'
|
| 127 |
+
)
|
| 128 |
+
# Barre B (droite)
|
| 129 |
+
b_width = (prop_b.get(cls, 0.0) / max_prop) * bar_max_width
|
| 130 |
+
if b_width > 0:
|
| 131 |
+
x_b = center + label_width // 2
|
| 132 |
+
parts.append(
|
| 133 |
+
f'<rect x="{x_b:.1f}" y="{y + 3}" '
|
| 134 |
+
f'width="{b_width:.1f}" height="{row_height - 6}" '
|
| 135 |
+
f'fill="{color}" stroke="#666" stroke-width="0.5" '
|
| 136 |
+
f'opacity="0.85"/>'
|
| 137 |
+
)
|
| 138 |
+
parts.append(
|
| 139 |
+
f'<text x="{x_b + b_width + 3:.1f}" '
|
| 140 |
+
f'y="{y + row_height // 2 + 4}" '
|
| 141 |
+
f'font-size="10" fill="#444" text-anchor="start">'
|
| 142 |
+
f'{prop_b.get(cls, 0.0) * 100:.1f}%</text>'
|
| 143 |
+
)
|
| 144 |
+
parts.append("</svg>")
|
| 145 |
+
return "".join(parts)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _build_recoverability_summary_html(
|
| 149 |
+
data: dict, labels: dict,
|
| 150 |
+
) -> str:
|
| 151 |
+
"""Encart résumé par catégorie de récupérabilité (3 lignes)."""
|
| 152 |
+
totals = data.get("totals_by_recoverability") or {}
|
| 153 |
+
if not totals:
|
| 154 |
+
return ""
|
| 155 |
+
label_recov = labels.get("taxocomp_recoverable", "Récupérable")
|
| 156 |
+
label_diff = labels.get("taxocomp_difficult", "Difficile")
|
| 157 |
+
label_irrec = labels.get("taxocomp_irrecoverable", "Irrécupérable")
|
| 158 |
+
rows = [
|
| 159 |
+
("recoverable", label_recov),
|
| 160 |
+
("difficult", label_diff),
|
| 161 |
+
("irrecoverable", label_irrec),
|
| 162 |
+
]
|
| 163 |
+
parts = [
|
| 164 |
+
'<table style="border-collapse:collapse;font-size:.85rem;'
|
| 165 |
+
'margin-top:.5rem">',
|
| 166 |
+
'<thead><tr>',
|
| 167 |
+
'<th scope=\"col\" style="padding:.2rem .5rem;text-align:left;'
|
| 168 |
+
'border-bottom:1px solid #ccc">'
|
| 169 |
+
f'{_e(labels.get("taxocomp_level_label", "Catégorie"))}</th>',
|
| 170 |
+
'<th scope=\"col\" style="padding:.2rem .5rem;text-align:right;'
|
| 171 |
+
'border-bottom:1px solid #ccc">'
|
| 172 |
+
f'{_e(_e(data["engine_a"]))}</th>',
|
| 173 |
+
'<th scope=\"col\" style="padding:.2rem .5rem;text-align:right;'
|
| 174 |
+
'border-bottom:1px solid #ccc">'
|
| 175 |
+
f'{_e(_e(data["engine_b"]))}</th>',
|
| 176 |
+
'</tr></thead><tbody>',
|
| 177 |
+
]
|
| 178 |
+
for level, label in rows:
|
| 179 |
+
cell = totals.get(level, {"a": 0.0, "b": 0.0})
|
| 180 |
+
color = _RECOVERABILITY_COLORS.get(level, "#888")
|
| 181 |
+
parts.append(
|
| 182 |
+
f'<tr>'
|
| 183 |
+
f'<td style="padding:.2rem .5rem">'
|
| 184 |
+
f'<span style="display:inline-block;width:10px;height:10px;'
|
| 185 |
+
f'background:{color};margin-right:.4rem;border-radius:2px"></span>'
|
| 186 |
+
f'{_e(label)}</td>'
|
| 187 |
+
f'<td style="padding:.2rem .5rem;text-align:right;'
|
| 188 |
+
f'font-family:monospace">{cell["a"] * 100:.1f}%</td>'
|
| 189 |
+
f'<td style="padding:.2rem .5rem;text-align:right;'
|
| 190 |
+
f'font-family:monospace">{cell["b"] * 100:.1f}%</td>'
|
| 191 |
+
f'</tr>'
|
| 192 |
+
)
|
| 193 |
+
parts.append("</tbody></table>")
|
| 194 |
+
return "".join(parts)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def build_taxonomy_comparison_html(
|
| 198 |
+
data: Optional[dict],
|
| 199 |
+
labels: Optional[dict[str, str]] = None,
|
| 200 |
+
) -> str:
|
| 201 |
+
"""Construit le bloc HTML de comparaison taxonomique entre 2 moteurs.
|
| 202 |
+
|
| 203 |
+
Retourne ``""`` si ``data is None`` ou aucune classe.
|
| 204 |
+
"""
|
| 205 |
+
if not data:
|
| 206 |
+
return ""
|
| 207 |
+
classes = data.get("classes") or []
|
| 208 |
+
if not classes:
|
| 209 |
+
return ""
|
| 210 |
+
labels = labels or {}
|
| 211 |
+
title_template = labels.get(
|
| 212 |
+
"taxocomp_title", "Profil taxonomique : {engine_a} vs {engine_b}",
|
| 213 |
+
)
|
| 214 |
+
title = title_template.format(
|
| 215 |
+
engine_a=data["engine_a"], engine_b=data["engine_b"],
|
| 216 |
+
)
|
| 217 |
+
note = labels.get(
|
| 218 |
+
"taxocomp_note",
|
| 219 |
+
"Diagramme miroir des proportions d'erreurs par classe. "
|
| 220 |
+
"Couleur selon récupérabilité éditoriale (vert = corrigeable, "
|
| 221 |
+
"rouge = irrécupérable). À CER global égal, un moteur dont les "
|
| 222 |
+
"erreurs sont majoritairement vertes est préférable pour une "
|
| 223 |
+
"édition critique.",
|
| 224 |
+
)
|
| 225 |
+
parts = [
|
| 226 |
+
'<div class="taxocomp" style="margin:1rem 0">',
|
| 227 |
+
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 228 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 229 |
+
f'{_e(note)}</div>',
|
| 230 |
+
_build_mirror_chart_svg(data),
|
| 231 |
+
_build_recoverability_summary_html(data, labels),
|
| 232 |
+
"</div>",
|
| 233 |
+
]
|
| 234 |
+
return "".join(parts)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
__all__ = [
|
| 238 |
+
"build_taxonomy_comparison_html",
|
| 239 |
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML de la heatmap de co-occurrence taxonomique — Sprint 75.
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.taxonomy_cooccurrence_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.taxonomy_cooccurrence``.
|
| 6 |
+
Le chemin legacy reste disponible via un shim avec
|
| 7 |
+
``DeprecationWarning`` ; suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
A.I.4 chantier 1 du plan d'évolution 2026.
|
| 10 |
+
|
| 11 |
+
Suite directe ``picarones/core/taxonomy_cooccurrence.py``. Pattern
|
| 12 |
+
identique aux autres rendus (Sprints 41/43/62/67/72/74) :
|
| 13 |
+
**server-side**, pas de JavaScript, anti-injection systématique.
|
| 14 |
+
|
| 15 |
+
Sortie typique
|
| 16 |
+
--------------
|
| 17 |
+
- ``build_taxonomy_cooccurrence_html(data, labels)`` produit un
|
| 18 |
+
bloc complet : titre + note d'usage + heatmap SVG + table des
|
| 19 |
+
paires les plus co-occurrentes.
|
| 20 |
+
- ``""`` retourné si ``data is None`` ou si la matrice est vide
|
| 21 |
+
(rapport adaptatif).
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
from html import escape as _e
|
| 27 |
+
from typing import Optional
|
| 28 |
+
|
| 29 |
+
from picarones.reports_v2._helpers.render_helpers import (
|
| 30 |
+
GRADIENT_TARGET_BLUE,
|
| 31 |
+
build_grid_svg,
|
| 32 |
+
color_single_gradient,
|
| 33 |
+
text_color_for_bg,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _build_jaccard_heatmap_svg(
|
| 38 |
+
classes: list[str],
|
| 39 |
+
matrix: dict[str, dict[str, float]],
|
| 40 |
+
*,
|
| 41 |
+
cell_size: int = 36,
|
| 42 |
+
label_left: int = 130,
|
| 43 |
+
label_top: int = 80,
|
| 44 |
+
) -> str:
|
| 45 |
+
"""Heatmap Jaccard de co-occurrence taxonomique.
|
| 46 |
+
|
| 47 |
+
Délègue à :func:`build_grid_svg` ; reste un wrapper local qui
|
| 48 |
+
encapsule les conventions spécifiques à la matrice symétrique
|
| 49 |
+
(valeur affichée seulement si > 0,05, étiquettes rotées).
|
| 50 |
+
"""
|
| 51 |
+
if not classes:
|
| 52 |
+
return ""
|
| 53 |
+
|
| 54 |
+
def cell_value(i: int, j: int) -> float:
|
| 55 |
+
return matrix.get(classes[i], {}).get(classes[j], 0.0)
|
| 56 |
+
|
| 57 |
+
return build_grid_svg(
|
| 58 |
+
n_rows=len(classes),
|
| 59 |
+
n_cols=len(classes),
|
| 60 |
+
row_label_fn=lambda i: classes[i],
|
| 61 |
+
col_label_fn=lambda j: classes[j],
|
| 62 |
+
cell_color_fn=lambda i, j: color_single_gradient(
|
| 63 |
+
cell_value(i, j), end_rgb=GRADIENT_TARGET_BLUE,
|
| 64 |
+
),
|
| 65 |
+
cell_text_fn=lambda i, j: (
|
| 66 |
+
f"{cell_value(i, j):.2f}" if cell_value(i, j) > 0.05 else None
|
| 67 |
+
),
|
| 68 |
+
cell_text_color_fn=lambda i, j: text_color_for_bg(cell_value(i, j)),
|
| 69 |
+
cell_w=cell_size,
|
| 70 |
+
cell_h=cell_size,
|
| 71 |
+
label_left=label_left,
|
| 72 |
+
label_top=label_top,
|
| 73 |
+
rotate_col_labels=True,
|
| 74 |
+
aria_label="Heatmap Jaccard co-occurrence taxonomique",
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _build_top_pairs_table(
|
| 79 |
+
top_pairs: list,
|
| 80 |
+
labels: dict,
|
| 81 |
+
) -> str:
|
| 82 |
+
"""Construit la table HTML des paires les plus co-occurrentes."""
|
| 83 |
+
if not top_pairs:
|
| 84 |
+
return ""
|
| 85 |
+
pair_label = labels.get("taxocooc_pair_label", "Paire")
|
| 86 |
+
jaccard_label = labels.get("taxocooc_jaccard_label", "Jaccard")
|
| 87 |
+
|
| 88 |
+
parts = [
|
| 89 |
+
'<table style="border-collapse:collapse;font-size:.85rem;'
|
| 90 |
+
'margin-top:.5rem">',
|
| 91 |
+
'<thead><tr>',
|
| 92 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 93 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 94 |
+
f'{_e(pair_label)}</th>',
|
| 95 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:right;'
|
| 96 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 97 |
+
f'{_e(jaccard_label)}</th>',
|
| 98 |
+
'</tr></thead><tbody>',
|
| 99 |
+
]
|
| 100 |
+
for ca, cb, j in top_pairs:
|
| 101 |
+
parts.append(
|
| 102 |
+
f'<tr>'
|
| 103 |
+
f'<td style="padding:.2rem .5rem">'
|
| 104 |
+
f'<code>{_e(ca)}</code> ↔ <code>{_e(cb)}</code></td>'
|
| 105 |
+
f'<td style="padding:.2rem .5rem;text-align:right;'
|
| 106 |
+
f'font-family:monospace;'
|
| 107 |
+
f'background:{color_single_gradient(j, end_rgb=GRADIENT_TARGET_BLUE)};'
|
| 108 |
+
f'color:{text_color_for_bg(j)}">{j:.2f}</td>'
|
| 109 |
+
f'</tr>'
|
| 110 |
+
)
|
| 111 |
+
parts.append("</tbody></table>")
|
| 112 |
+
return "".join(parts)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def build_taxonomy_cooccurrence_html(
|
| 116 |
+
data: Optional[dict],
|
| 117 |
+
labels: Optional[dict[str, str]] = None,
|
| 118 |
+
) -> str:
|
| 119 |
+
"""Construit le bloc HTML complet de co-occurrence taxonomique.
|
| 120 |
+
|
| 121 |
+
Retourne ``""`` si ``data is None`` ou matrice vide.
|
| 122 |
+
"""
|
| 123 |
+
if not data:
|
| 124 |
+
return ""
|
| 125 |
+
classes = data.get("classes") or []
|
| 126 |
+
matrix = data.get("cooccurrence_matrix") or {}
|
| 127 |
+
if not classes or not matrix:
|
| 128 |
+
return ""
|
| 129 |
+
labels = labels or {}
|
| 130 |
+
title = labels.get(
|
| 131 |
+
"taxocooc_title",
|
| 132 |
+
"Co-occurrence des classes d'erreur",
|
| 133 |
+
)
|
| 134 |
+
note = labels.get(
|
| 135 |
+
"taxocooc_note",
|
| 136 |
+
"Indice de Jaccard au niveau document : 1,00 = ces deux classes "
|
| 137 |
+
"apparaissent toujours ensemble ; 0,00 = jamais. Lecture par paires "
|
| 138 |
+
"co-occurrentes ci-dessous.",
|
| 139 |
+
)
|
| 140 |
+
n_docs = data.get("n_documents", 0)
|
| 141 |
+
n_docs_label_template = labels.get(
|
| 142 |
+
"taxocooc_n_docs", "Calculé sur {n_docs} documents.",
|
| 143 |
+
)
|
| 144 |
+
n_docs_phrase = n_docs_label_template.format(n_docs=n_docs)
|
| 145 |
+
|
| 146 |
+
svg = _build_jaccard_heatmap_svg(classes, matrix)
|
| 147 |
+
top_table = _build_top_pairs_table(
|
| 148 |
+
data.get("top_pairs") or [], labels,
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
parts = [
|
| 152 |
+
'<div class="taxocooc" style="margin:1rem 0">',
|
| 153 |
+
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 154 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 155 |
+
f'{_e(note)}</div>',
|
| 156 |
+
f'<div style="font-size:.8rem;opacity:.7;margin-bottom:.5rem">'
|
| 157 |
+
f'{_e(n_docs_phrase)}</div>',
|
| 158 |
+
svg,
|
| 159 |
+
top_table,
|
| 160 |
+
"</div>",
|
| 161 |
+
]
|
| 162 |
+
return "".join(parts)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
__all__ = [
|
| 166 |
+
"build_taxonomy_cooccurrence_html",
|
| 167 |
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML de la heatmap class × position — Sprint 76.
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.taxonomy_intra_doc_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.taxonomy_intra_doc``. Le chemin
|
| 6 |
+
legacy reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 7 |
+
suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
A.I.4 chantier 2 du plan d'évolution 2026.
|
| 10 |
+
|
| 11 |
+
Suite directe ``picarones/core/taxonomy_intra_doc.py``. Pattern
|
| 12 |
+
identique aux autres rendus (Sprints 41/43/62/67/72/74/75) :
|
| 13 |
+
**server-side**, pas de JavaScript, anti-injection systématique.
|
| 14 |
+
|
| 15 |
+
Sortie typique
|
| 16 |
+
--------------
|
| 17 |
+
Une grille N_classes × N_bins où chaque cellule indique la densité
|
| 18 |
+
d'erreurs de cette classe à cette position dans le document.
|
| 19 |
+
Lecture immédiate : « ligature_error concentré dans la première
|
| 20 |
+
tranche → erreur de marge ; visual_confusion uniformément réparti
|
| 21 |
+
→ erreur de scribe ».
|
| 22 |
+
|
| 23 |
+
Adaptive : si ``data is None`` ou si toutes les classes ont 0
|
| 24 |
+
erreur, retourne ``""``.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
from html import escape as _e
|
| 30 |
+
from typing import Optional
|
| 31 |
+
|
| 32 |
+
from picarones.reports_v2._helpers.render_helpers import (
|
| 33 |
+
GRADIENT_TARGET_ORANGE,
|
| 34 |
+
build_grid_svg,
|
| 35 |
+
color_single_gradient,
|
| 36 |
+
text_color_for_bg,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _build_position_heatmap_svg(
|
| 41 |
+
classes_with_errors: list[str],
|
| 42 |
+
per_class: dict[str, list[int]],
|
| 43 |
+
n_bins: int,
|
| 44 |
+
*,
|
| 45 |
+
cell_w: int = 36,
|
| 46 |
+
cell_h: int = 26,
|
| 47 |
+
label_left: int = 150,
|
| 48 |
+
label_top: int = 30,
|
| 49 |
+
) -> str:
|
| 50 |
+
"""Heatmap class taxonomique × position (densité relative par classe).
|
| 51 |
+
|
| 52 |
+
Délègue à :func:`build_grid_svg` ; reste un wrapper local qui
|
| 53 |
+
encapsule la normalisation par classe (densité relative au max
|
| 54 |
+
observé sur la ligne).
|
| 55 |
+
"""
|
| 56 |
+
if not classes_with_errors:
|
| 57 |
+
return ""
|
| 58 |
+
|
| 59 |
+
# Pré-calcule densité et count par cellule pour éviter les boucles
|
| 60 |
+
# imbriquées dans les callbacks.
|
| 61 |
+
grid: list[list[tuple[int, float]]] = []
|
| 62 |
+
for cls in classes_with_errors:
|
| 63 |
+
counts = per_class.get(cls, [0] * n_bins)
|
| 64 |
+
max_count = max(counts) if counts else 0
|
| 65 |
+
row: list[tuple[int, float]] = []
|
| 66 |
+
for j in range(n_bins):
|
| 67 |
+
count = counts[j] if j < len(counts) else 0
|
| 68 |
+
density = (count / max_count) if max_count > 0 else 0.0
|
| 69 |
+
row.append((count, density))
|
| 70 |
+
grid.append(row)
|
| 71 |
+
|
| 72 |
+
return build_grid_svg(
|
| 73 |
+
n_rows=len(classes_with_errors),
|
| 74 |
+
n_cols=n_bins,
|
| 75 |
+
row_label_fn=lambda i: classes_with_errors[i],
|
| 76 |
+
col_label_fn=lambda j: str(j + 1),
|
| 77 |
+
cell_color_fn=lambda i, j: color_single_gradient(
|
| 78 |
+
grid[i][j][1], end_rgb=GRADIENT_TARGET_ORANGE,
|
| 79 |
+
),
|
| 80 |
+
cell_text_fn=lambda i, j: (
|
| 81 |
+
str(grid[i][j][0]) if grid[i][j][0] > 0 else None
|
| 82 |
+
),
|
| 83 |
+
cell_text_color_fn=lambda i, j: text_color_for_bg(grid[i][j][1]),
|
| 84 |
+
cell_w=cell_w,
|
| 85 |
+
cell_h=cell_h,
|
| 86 |
+
label_left=label_left,
|
| 87 |
+
label_top=label_top,
|
| 88 |
+
aria_label="Heatmap class taxonomique × position",
|
| 89 |
+
x_axis_title="Position dans le document (1 = début)",
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def build_taxonomy_intra_doc_html(
|
| 94 |
+
data: Optional[dict],
|
| 95 |
+
labels: Optional[dict[str, str]] = None,
|
| 96 |
+
) -> str:
|
| 97 |
+
"""Construit le bloc HTML complet de la heatmap intra-document.
|
| 98 |
+
|
| 99 |
+
Retourne ``""`` si ``data is None`` ou aucune erreur.
|
| 100 |
+
"""
|
| 101 |
+
if not data:
|
| 102 |
+
return ""
|
| 103 |
+
n_bins = data.get("n_bins", 0)
|
| 104 |
+
per_class = data.get("per_class") or {}
|
| 105 |
+
total_errors = data.get("total_errors", 0)
|
| 106 |
+
if total_errors == 0 or n_bins <= 0:
|
| 107 |
+
return ""
|
| 108 |
+
# Filtre : uniquement les classes ayant au moins une erreur
|
| 109 |
+
classes_with_errors = [
|
| 110 |
+
cls for cls, counts in per_class.items()
|
| 111 |
+
if isinstance(counts, list) and sum(counts) > 0
|
| 112 |
+
]
|
| 113 |
+
if not classes_with_errors:
|
| 114 |
+
return ""
|
| 115 |
+
|
| 116 |
+
labels = labels or {}
|
| 117 |
+
title = labels.get(
|
| 118 |
+
"intradoc_title",
|
| 119 |
+
"Évolution intra-document des classes d'erreur",
|
| 120 |
+
)
|
| 121 |
+
note = labels.get(
|
| 122 |
+
"intradoc_note",
|
| 123 |
+
"Heatmap class × position : densité relative par classe "
|
| 124 |
+
"(plus foncé = concentré). Une classe concentrée dans la "
|
| 125 |
+
"première colonne suggère une erreur de marge ; "
|
| 126 |
+
"une distribution uniforme suggère une erreur de scribe.",
|
| 127 |
+
)
|
| 128 |
+
n_words_gt = data.get("n_words_gt", 0)
|
| 129 |
+
n_words_template = labels.get(
|
| 130 |
+
"intradoc_n_words",
|
| 131 |
+
"Calculé sur {n_words_gt} mots GT, répartis en {n_bins} tranches.",
|
| 132 |
+
)
|
| 133 |
+
n_words_phrase = n_words_template.format(
|
| 134 |
+
n_words_gt=n_words_gt, n_bins=n_bins,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
svg = _build_position_heatmap_svg(classes_with_errors, per_class, n_bins)
|
| 138 |
+
|
| 139 |
+
parts = [
|
| 140 |
+
'<div class="intradoc" style="margin:1rem 0">',
|
| 141 |
+
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 142 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 143 |
+
f'{_e(note)}</div>',
|
| 144 |
+
f'<div style="font-size:.8rem;opacity:.7;margin-bottom:.5rem">'
|
| 145 |
+
f'{_e(n_words_phrase)}</div>',
|
| 146 |
+
svg,
|
| 147 |
+
"</div>",
|
| 148 |
+
]
|
| 149 |
+
return "".join(parts)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
__all__ = [
|
| 153 |
+
"build_taxonomy_intra_doc_html",
|
| 154 |
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML de la vue « Worst lines globale » — Sprint 72.
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.worst_lines_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.worst_lines``. Le chemin
|
| 6 |
+
legacy reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 7 |
+
suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Suite directe de ``picarones/core/worst_lines.py`` (extraction
|
| 10 |
+
transversale). Pattern identique aux Sprints 41/43/62/67 : rendu
|
| 11 |
+
**server-side**, pas de JavaScript, anti-injection systématique
|
| 12 |
+
via ``html.escape``.
|
| 13 |
+
|
| 14 |
+
Vue distincte du tableau gallery existant
|
| 15 |
+
-----------------------------------------
|
| 16 |
+
La galerie OCR (vue ``view_gallery.html``) liste les documents
|
| 17 |
+
les plus problématiques. Cette vue va plus fin : elle liste les
|
| 18 |
+
**lignes individuelles** les plus problématiques, transversalement
|
| 19 |
+
à tous les documents et moteurs. Complémentaire, pas redondante.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
from html import escape as _e
|
| 25 |
+
from typing import Optional
|
| 26 |
+
|
| 27 |
+
from picarones.evaluation.metrics.worst_lines import WorstLineEntry
|
| 28 |
+
from picarones.evaluation import compute_char_diff
|
| 29 |
+
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _bg_for_cer(cer: float) -> str:
|
| 33 |
+
"""Beige clair sous le seuil catastrophique (0.30), gradient
|
| 34 |
+
jaune → rouge au-delà.
|
| 35 |
+
|
| 36 |
+
Le seuil dur à 0.30 préserve la sémantique « toléré jusqu'à 30 %
|
| 37 |
+
pour un manuscrit difficile ». Au-delà, on entre en zone visible
|
| 38 |
+
avec :func:`color_traffic_light` (low_is_good).
|
| 39 |
+
"""
|
| 40 |
+
f = max(0.0, min(1.0, cer))
|
| 41 |
+
if f < 0.3:
|
| 42 |
+
return "#fff8dc"
|
| 43 |
+
return color_traffic_light(f, low_is_good=True, scale_min=0.3, scale_max=1.0)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _render_diff_inline(reference: str, hypothesis: str) -> str:
|
| 47 |
+
"""Rendu HTML inline d'un diff caractère par caractère.
|
| 48 |
+
|
| 49 |
+
- ``equal`` → texte normal
|
| 50 |
+
- ``delete`` → fond rouge clair, barré (manquait dans hyp)
|
| 51 |
+
- ``insert`` → fond vert clair (ajouté par hyp)
|
| 52 |
+
- ``replace`` → fond rouge clair barré + fond vert clair pour
|
| 53 |
+
la nouvelle valeur (côte à côte)
|
| 54 |
+
"""
|
| 55 |
+
if not reference and not hypothesis:
|
| 56 |
+
return '<span style="opacity:.5">∅</span>'
|
| 57 |
+
ops = compute_char_diff(reference or "", hypothesis or "")
|
| 58 |
+
parts: list[str] = []
|
| 59 |
+
for op in ops:
|
| 60 |
+
kind = op["op"]
|
| 61 |
+
if kind == "equal":
|
| 62 |
+
parts.append(_e(op["text"]))
|
| 63 |
+
elif kind == "delete":
|
| 64 |
+
parts.append(
|
| 65 |
+
f'<span style="background:#fdd;text-decoration:line-through">'
|
| 66 |
+
f'{_e(op["text"])}</span>'
|
| 67 |
+
)
|
| 68 |
+
elif kind == "insert":
|
| 69 |
+
parts.append(
|
| 70 |
+
f'<span style="background:#dfd">{_e(op["text"])}</span>'
|
| 71 |
+
)
|
| 72 |
+
elif kind == "replace":
|
| 73 |
+
parts.append(
|
| 74 |
+
f'<span style="background:#fdd;text-decoration:line-through">'
|
| 75 |
+
f'{_e(op["old"])}</span>'
|
| 76 |
+
f'<span style="background:#dfd">{_e(op["new"])}</span>'
|
| 77 |
+
)
|
| 78 |
+
return "".join(parts)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def build_worst_lines_table_html(
|
| 82 |
+
entries: list[WorstLineEntry],
|
| 83 |
+
labels: Optional[dict[str, str]] = None,
|
| 84 |
+
) -> str:
|
| 85 |
+
"""Construit le tableau HTML des worst lines.
|
| 86 |
+
|
| 87 |
+
Retourne ``""`` si la liste est vide. Adaptive : si aucune
|
| 88 |
+
entrée n'a de ``script_type``, la colonne strate est omise.
|
| 89 |
+
"""
|
| 90 |
+
if not entries:
|
| 91 |
+
return ""
|
| 92 |
+
labels = labels or {}
|
| 93 |
+
title = labels.get("worst_lines_title", "Lignes les plus problématiques")
|
| 94 |
+
note = labels.get(
|
| 95 |
+
"worst_lines_note",
|
| 96 |
+
"Top-N lignes du corpus classées par CER ligne décroissant. "
|
| 97 |
+
"Diff caractère par caractère : rouge barré = manquant dans "
|
| 98 |
+
"l'OCR, vert = ajouté par l'OCR.",
|
| 99 |
+
)
|
| 100 |
+
rank_label = labels.get("worst_lines_rank_label", "Rang")
|
| 101 |
+
cer_label = labels.get("worst_lines_cer_label", "CER")
|
| 102 |
+
engine_label = labels.get("worst_lines_engine_label", "Moteur")
|
| 103 |
+
doc_label = labels.get("worst_lines_doc_label", "Document")
|
| 104 |
+
line_label = labels.get("worst_lines_line_label", "Ligne #")
|
| 105 |
+
strata_label = labels.get("worst_lines_strata_label", "Strate")
|
| 106 |
+
diff_label = labels.get("worst_lines_diff_label", "GT → OCR (diff)")
|
| 107 |
+
|
| 108 |
+
has_strata = any(e.script_type for e in entries)
|
| 109 |
+
|
| 110 |
+
parts = [
|
| 111 |
+
'<div class="worst-lines" style="margin:1rem 0">',
|
| 112 |
+
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 113 |
+
f'<div style="font-size:.8rem;opacity:.75;margin-bottom:.5rem">'
|
| 114 |
+
f'{_e(note)}</div>',
|
| 115 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 116 |
+
'font-size:.85rem">',
|
| 117 |
+
'<thead><tr>',
|
| 118 |
+
]
|
| 119 |
+
cols = [rank_label, cer_label, engine_label, doc_label, line_label]
|
| 120 |
+
if has_strata:
|
| 121 |
+
cols.append(strata_label)
|
| 122 |
+
cols.append(diff_label)
|
| 123 |
+
for col in cols:
|
| 124 |
+
parts.append(
|
| 125 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 126 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 127 |
+
f'{_e(col)}</th>'
|
| 128 |
+
)
|
| 129 |
+
parts.append("</tr></thead><tbody>")
|
| 130 |
+
for entry in entries:
|
| 131 |
+
cer_color = _bg_for_cer(entry.cer)
|
| 132 |
+
parts.append("<tr>")
|
| 133 |
+
parts.append(
|
| 134 |
+
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 135 |
+
f'font-weight:600">{entry.rank}</td>'
|
| 136 |
+
)
|
| 137 |
+
parts.append(
|
| 138 |
+
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 139 |
+
f'background:{cer_color};font-family:monospace">'
|
| 140 |
+
f'{entry.cer * 100:.1f}%</td>'
|
| 141 |
+
)
|
| 142 |
+
parts.append(
|
| 143 |
+
f'<td style="padding:.3rem .5rem">{_e(entry.engine_name)}</td>'
|
| 144 |
+
)
|
| 145 |
+
parts.append(
|
| 146 |
+
f'<td style="padding:.3rem .5rem;font-family:monospace;'
|
| 147 |
+
f'font-size:.8rem">{_e(entry.doc_id)}</td>'
|
| 148 |
+
)
|
| 149 |
+
parts.append(
|
| 150 |
+
f'<td style="padding:.3rem .5rem;text-align:right">'
|
| 151 |
+
f'{entry.line_index}</td>'
|
| 152 |
+
)
|
| 153 |
+
if has_strata:
|
| 154 |
+
parts.append(
|
| 155 |
+
f'<td style="padding:.3rem .5rem;font-size:.8rem">'
|
| 156 |
+
f'{_e(entry.script_type or "—")}</td>'
|
| 157 |
+
)
|
| 158 |
+
parts.append(
|
| 159 |
+
f'<td style="padding:.3rem .5rem;font-family:monospace;'
|
| 160 |
+
f'font-size:.85rem">'
|
| 161 |
+
f'{_render_diff_inline(entry.gt_line, entry.hyp_line)}</td>'
|
| 162 |
+
)
|
| 163 |
+
parts.append("</tr>")
|
| 164 |
+
parts.append("</tbody></table></div>")
|
| 165 |
+
return "".join(parts)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
__all__ = [
|
| 169 |
+
"build_worst_lines_table_html",
|
| 170 |
+
]
|
|
@@ -97,7 +97,7 @@ class TestTaxonomyCooccurrence:
|
|
| 97 |
assert r == results[0]
|
| 98 |
|
| 99 |
def test_compatible_with_renderer(self, sample_benchmark) -> None:
|
| 100 |
-
from picarones.
|
| 101 |
build_taxonomy_cooccurrence_html,
|
| 102 |
)
|
| 103 |
result = compute_taxonomy_cooccurrence_section(sample_benchmark)
|
|
@@ -138,7 +138,7 @@ class TestTaxonomyIntraDoc:
|
|
| 138 |
assert key in result, f"clé {key!r} manquante (renderer la requiert)"
|
| 139 |
|
| 140 |
def test_renders_html_when_signal_present(self, sample_benchmark) -> None:
|
| 141 |
-
from picarones.
|
| 142 |
build_taxonomy_intra_doc_html,
|
| 143 |
)
|
| 144 |
result = compute_taxonomy_intra_doc_section(sample_benchmark)
|
|
|
|
| 97 |
assert r == results[0]
|
| 98 |
|
| 99 |
def test_compatible_with_renderer(self, sample_benchmark) -> None:
|
| 100 |
+
from picarones.reports_v2.html.renderers.taxonomy_cooccurrence import (
|
| 101 |
build_taxonomy_cooccurrence_html,
|
| 102 |
)
|
| 103 |
result = compute_taxonomy_cooccurrence_section(sample_benchmark)
|
|
|
|
| 138 |
assert key in result, f"clé {key!r} manquante (renderer la requiert)"
|
| 139 |
|
| 140 |
def test_renders_html_when_signal_present(self, sample_benchmark) -> None:
|
| 141 |
+
from picarones.reports_v2.html.renderers.taxonomy_intra_doc import (
|
| 142 |
build_taxonomy_intra_doc_html,
|
| 143 |
)
|
| 144 |
result = compute_taxonomy_intra_doc_section(sample_benchmark)
|
|
@@ -28,7 +28,7 @@ from dataclasses import dataclass, field
|
|
| 28 |
from typing import Any
|
| 29 |
|
| 30 |
from picarones.measurements.worst_lines import WorstLineEntry, extract_worst_lines
|
| 31 |
-
from picarones.
|
| 32 |
|
| 33 |
|
| 34 |
# ──────────────────────────────────────────────────────────────────────────
|
|
|
|
| 28 |
from typing import Any
|
| 29 |
|
| 30 |
from picarones.measurements.worst_lines import WorstLineEntry, extract_worst_lines
|
| 31 |
+
from picarones.reports_v2.html.renderers.worst_lines import build_worst_lines_table_html
|
| 32 |
|
| 33 |
|
| 34 |
# ──────────────────────────────────────────────────────────────────────────
|
|
@@ -29,7 +29,7 @@ import pytest
|
|
| 29 |
from picarones.measurements.taxonomy_cooccurrence import (
|
| 30 |
compute_taxonomy_cooccurrence,
|
| 31 |
)
|
| 32 |
-
from picarones.
|
| 33 |
build_taxonomy_cooccurrence_html,
|
| 34 |
)
|
| 35 |
|
|
|
|
| 29 |
from picarones.measurements.taxonomy_cooccurrence import (
|
| 30 |
compute_taxonomy_cooccurrence,
|
| 31 |
)
|
| 32 |
+
from picarones.reports_v2.html.renderers.taxonomy_cooccurrence import (
|
| 33 |
build_taxonomy_cooccurrence_html,
|
| 34 |
)
|
| 35 |
|
|
@@ -29,7 +29,7 @@ import pytest
|
|
| 29 |
from picarones.measurements.taxonomy_intra_doc import (
|
| 30 |
compute_taxonomy_position_heatmap,
|
| 31 |
)
|
| 32 |
-
from picarones.
|
| 33 |
build_taxonomy_intra_doc_html,
|
| 34 |
)
|
| 35 |
|
|
|
|
| 29 |
from picarones.measurements.taxonomy_intra_doc import (
|
| 30 |
compute_taxonomy_position_heatmap,
|
| 31 |
)
|
| 32 |
+
from picarones.reports_v2.html.renderers.taxonomy_intra_doc import (
|
| 33 |
build_taxonomy_intra_doc_html,
|
| 34 |
)
|
| 35 |
|
|
@@ -27,7 +27,7 @@ from picarones.measurements.taxonomy_comparison import (
|
|
| 27 |
RECOVERABILITY,
|
| 28 |
compare_taxonomies,
|
| 29 |
)
|
| 30 |
-
from picarones.
|
| 31 |
build_taxonomy_comparison_html,
|
| 32 |
)
|
| 33 |
|
|
|
|
| 27 |
RECOVERABILITY,
|
| 28 |
compare_taxonomies,
|
| 29 |
)
|
| 30 |
+
from picarones.reports_v2.html.renderers.taxonomy_comparison import (
|
| 31 |
build_taxonomy_comparison_html,
|
| 32 |
)
|
| 33 |
|
|
@@ -21,7 +21,7 @@ from __future__ import annotations
|
|
| 21 |
import json
|
| 22 |
from pathlib import Path
|
| 23 |
|
| 24 |
-
from picarones.
|
| 25 |
|
| 26 |
|
| 27 |
def _load_labels(lang: str) -> dict:
|
|
|
|
| 21 |
import json
|
| 22 |
from pathlib import Path
|
| 23 |
|
| 24 |
+
from picarones.reports_v2.html.renderers.pipeline_dag import build_pipeline_dag_html
|
| 25 |
|
| 26 |
|
| 27 |
def _load_labels(lang: str) -> dict:
|