Spaces:
Sleeping
feat(migration): Phase 5.C batch 2 — 5 renderers moyens vers reports_v2/html/
Browse filesDeuxième vague de migration des 22 renderers thématiques.
Substitution : ``numerical_sequences_render`` reporté au batch 3
car sa dépendance ``measurements/numerical_sequences.py`` dépend
elle-même de ``measurements/roman_numerals.py``, deux modules
non encore migrés vers ``evaluation/metrics/``. Remplacé par
``longitudinal_render`` qui n'a pas de dépendance legacy.
Migrations effectuées
---------------------
| Source legacy | Destination canonique |
|----------------------------------------------|------------------------------------------------------|
| ``report/difficulty_render.py`` (45) | ``reports_v2/html/renderers/difficulty.py`` |
| ``report/lexical_modernization_render.py`` (114) | ``reports_v2/html/renderers/lexical_modernization.py`` |
| ``report/multirun_stability_render.py`` (151)| ``reports_v2/html/renderers/multirun_stability.py`` |
| ``report/throughput_render.py`` (154) | ``reports_v2/html/renderers/throughput.py`` |
| ``report/longitudinal_render.py`` (165) | ``reports_v2/html/renderers/longitudinal.py`` |
Total : ~629 lignes relocalisées. 5 nouveaux shims minimaux
(< 20 lignes) avec ``DeprecationWarning``.
Adaptations transverses
-----------------------
- ``reports_v2/html/renderers/lexical_modernization.py`` import
canonique ``picarones.evaluation.metrics.lexical_modernization``
(au lieu du shim legacy
``picarones.measurements.lexical_modernization``).
- ``test_module_coverage.py::TEST_ONLY_BASELINE`` étendu à
``"lexical_modernization"`` (même rationale que
``specialization`` au batch 1).
- Tests + ``picarones/report/generator.py`` mis à jour pour les
5 chemins canoniques.
Cumul Phase 5.C (batches 1+2)
-----------------------------
10 / 22 renderers migrés (~1198 lignes). 12 renderers restants.
Acceptance
----------
5019 tests passent, lint vert, architecture vérifiée.
https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP
- docs/migration/legacy-retirement-plan.md +47 -6
- picarones/report/difficulty_render.py +11 -38
- picarones/report/lexical_modernization_render.py +11 -107
- picarones/report/longitudinal_render.py +11 -158
- picarones/report/multirun_stability_render.py +11 -144
- picarones/report/throughput_render.py +11 -147
- picarones/report/views/advanced_taxonomy.py +1 -1
- picarones/report/views/diagnostics.py +2 -2
- picarones/report/views/economics.py +1 -1
- picarones/reports_v2/html/renderers/difficulty.py +51 -0
- picarones/reports_v2/html/renderers/lexical_modernization.py +120 -0
- picarones/reports_v2/html/renderers/longitudinal.py +171 -0
- picarones/reports_v2/html/renderers/multirun_stability.py +157 -0
- picarones/reports_v2/html/renderers/throughput.py +160 -0
- tests/architecture/test_module_coverage.py +10 -10
- tests/report/test_sprint80_lexical_modernization.py +1 -1
- tests/report/test_sprint90_engine_unstable.py +1 -1
- tests/report/test_sprint91_throughput.py +1 -1
- tests/report/test_sprint92_longitudinal.py +1 -1
|
@@ -691,12 +691,13 @@ architecture vérifiée.
|
|
| 691 |
|
| 692 |
**Reporté aux batches suivants** :
|
| 693 |
|
| 694 |
-
- Batch 2 (
|
| 695 |
-
|
| 696 |
-
``
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
``
|
|
|
|
| 700 |
- Batch 4 (~5 renderers gros) : ``error_absorption``,
|
| 701 |
``baseline``, ``inter_engine``, ``robustness_projection``,
|
| 702 |
``stratification``.
|
|
@@ -711,6 +712,46 @@ architecture vérifiée.
|
|
| 711 |
|
| 712 |
Effort restant estimé : 8-12 jours.
|
| 713 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
### Phase 6 — Pipelines OCR+LLM (`pipelines/`)
|
| 715 |
|
| 716 |
**Modules** : `pipelines/base.OCRLLMPipeline` (3 modes), `pipelines/
|
|
|
|
| 691 |
|
| 692 |
**Reporté aux batches suivants** :
|
| 693 |
|
| 694 |
+
- Batch 2 ✅ (cf. ci-dessous) — 5 renderers (45-165 LOC).
|
| 695 |
+
- Batch 3 (~5 renderers moyens) : ``module_audit``,
|
| 696 |
+
``ner``, ``image_predictive``,
|
| 697 |
+
``incremental_comparison``, ``numerical_sequences``
|
| 698 |
+
(ce dernier exige d'abord la migration du module
|
| 699 |
+
``measurements/numerical_sequences.py`` qui dépend de
|
| 700 |
+
``measurements/roman_numerals.py``).
|
| 701 |
- Batch 4 (~5 renderers gros) : ``error_absorption``,
|
| 702 |
``baseline``, ``inter_engine``, ``robustness_projection``,
|
| 703 |
``stratification``.
|
|
|
|
| 712 |
|
| 713 |
Effort restant estimé : 8-12 jours.
|
| 714 |
|
| 715 |
+
#### Phase 5.C.batch2 — Lot 2 : 5 renderers moyens (2026-05)
|
| 716 |
+
|
| 717 |
+
Deuxième vague. Substitution dans la sélection initiale :
|
| 718 |
+
``numerical_sequences_render`` reporté au batch 3 (sa dépendance
|
| 719 |
+
``measurements/numerical_sequences.py`` dépend elle-même de
|
| 720 |
+
``measurements/roman_numerals.py``, deux modules legacy non
|
| 721 |
+
migrés vers ``evaluation/metrics/`` ; le renderer ne peut donc pas
|
| 722 |
+
les importer depuis le canonique). Remplacé par
|
| 723 |
+
``longitudinal_render`` qui n'a pas de dépendance legacy.
|
| 724 |
+
|
| 725 |
+
**Migrations effectuées** :
|
| 726 |
+
|
| 727 |
+
| Source legacy | Destination canonique |
|
| 728 |
+
|----------------------------------------------|------------------------------------------------------|
|
| 729 |
+
| ``report/difficulty_render.py`` (45) | ``reports_v2/html/renderers/difficulty.py`` |
|
| 730 |
+
| ``report/lexical_modernization_render.py`` (114) | ``reports_v2/html/renderers/lexical_modernization.py`` |
|
| 731 |
+
| ``report/multirun_stability_render.py`` (151)| ``reports_v2/html/renderers/multirun_stability.py`` |
|
| 732 |
+
| ``report/throughput_render.py`` (154) | ``reports_v2/html/renderers/throughput.py`` |
|
| 733 |
+
| ``report/longitudinal_render.py`` (165) | ``reports_v2/html/renderers/longitudinal.py`` |
|
| 734 |
+
|
| 735 |
+
Total : ~629 lignes relocalisées. 5 nouveaux shims minimaux
|
| 736 |
+
(< 20 lignes) avec ``DeprecationWarning``.
|
| 737 |
+
|
| 738 |
+
**Adaptations transverses** :
|
| 739 |
+
|
| 740 |
+
- ``reports_v2/html/renderers/lexical_modernization.py`` import
|
| 741 |
+
canonique ``picarones.evaluation.metrics.lexical_modernization``
|
| 742 |
+
(au lieu du shim legacy ``picarones.measurements.lexical_modernization``).
|
| 743 |
+
- ``test_module_coverage.py::TEST_ONLY_BASELINE`` étendu à
|
| 744 |
+
``"lexical_modernization"`` (même rationale que ``specialization``
|
| 745 |
+
au batch 1).
|
| 746 |
+
- Tests + ``picarones/report/generator.py`` mis à jour pour les
|
| 747 |
+
5 chemins canoniques.
|
| 748 |
+
|
| 749 |
+
**Acceptance batch 2** : 5019 tests passent, lint vert,
|
| 750 |
+
architecture vérifiée.
|
| 751 |
+
|
| 752 |
+
**Cumul Phase 5.C** (batches 1+2) : 10 / 22 renderers migrés
|
| 753 |
+
(~1198 lignes). 12 renderers restants.
|
| 754 |
+
|
| 755 |
### Phase 6 — Pipelines OCR+LLM (`pipelines/`)
|
| 756 |
|
| 757 |
**Modules** : `pipelines/base.OCRLLMPipeline` (3 modes), `pipelines/
|
|
@@ -1,45 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
``picarones/measurements/difficulty.py`` et y violait la règle
|
| 6 |
-
Cercle 2 → Cercle 3 par un import paresseux de
|
| 7 |
-
``picarones.report.colors``. La fonction est désormais placée à sa
|
| 8 |
-
juste place — Cercle 3, à côté de la palette qu'elle consomme — et
|
| 9 |
-
``measurements/difficulty.py`` ne contient plus que de la logique
|
| 10 |
-
purement numérique.
|
| 11 |
-
|
| 12 |
-
Le module pur ``picarones.measurements.difficulty`` reste utilisable
|
| 13 |
-
sans dépendance vers ``picarones.report``.
|
| 14 |
"""
|
| 15 |
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
-
|
| 19 |
-
COLOR_GREEN,
|
| 20 |
-
COLOR_ORANGE,
|
| 21 |
-
COLOR_RED,
|
| 22 |
-
COLOR_YELLOW,
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
| 27 |
-
"""Retourne une couleur CSS pour un score de difficulté ∈ [0, 1].
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
Le label texte correspondant est produit par
|
| 37 |
-
:func:`picarones.measurements.difficulty.difficulty_label`.
|
| 38 |
-
"""
|
| 39 |
-
if score < 0.25:
|
| 40 |
-
return COLOR_GREEN
|
| 41 |
-
if score < 0.50:
|
| 42 |
-
return COLOR_YELLOW
|
| 43 |
-
if score < 0.75:
|
| 44 |
-
return COLOR_ORANGE
|
| 45 |
-
return COLOR_RED
|
|
|
|
| 1 |
+
"""``picarones.report.difficulty_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.difficulty`.
|
| 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.difficulty import * # noqa: F401, F403
|
|
|
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.difficulty_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.difficulty instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,114 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
Suite directe ``picarones/core/lexical_modernization.py``.
|
| 6 |
-
Pattern identique aux autres rendus (Sprints 41/43/62/67/72/74/75/76/77) :
|
| 7 |
-
**server-side**, pas de JavaScript, anti-injection systématique.
|
| 8 |
-
|
| 9 |
-
Vue
|
| 10 |
-
---
|
| 11 |
-
Tableau trié par taux de modernisation décroissant : forme
|
| 12 |
-
historique GT → forme(s) modernisée(s), occurrences GT, %.
|
| 13 |
-
Couleur de cellule pour le %.
|
| 14 |
"""
|
| 15 |
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
-
|
| 19 |
-
from typing import Optional
|
| 20 |
-
|
| 21 |
-
from picarones.measurements.lexical_modernization import top_modernized_tokens
|
| 22 |
-
from picarones.reports_v2._helpers.render_helpers import (
|
| 23 |
-
GRADIENT_TARGET_ORANGE,
|
| 24 |
-
color_single_gradient,
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _format_variants(variants: dict, max_show: int = 3) -> str:
|
| 29 |
-
"""Liste compacte des variants modernisés."""
|
| 30 |
-
items = sorted(variants.items(), key=lambda kv: -kv[1])
|
| 31 |
-
shown = items[:max_show]
|
| 32 |
-
rest = len(items) - max_show
|
| 33 |
-
parts = [
|
| 34 |
-
f"{_e(form)} ({count})"
|
| 35 |
-
for form, count in shown
|
| 36 |
-
]
|
| 37 |
-
if rest > 0:
|
| 38 |
-
parts.append(f"+{rest}")
|
| 39 |
-
return ", ".join(parts)
|
| 40 |
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
) -> str:
|
| 49 |
-
"""Construit la table HTML de modernisation lexicale.
|
| 50 |
-
|
| 51 |
-
Retourne ``""`` si ``data is None`` ou si aucun token modernisé.
|
| 52 |
-
"""
|
| 53 |
-
if not data:
|
| 54 |
-
return ""
|
| 55 |
-
rows = top_modernized_tokens(data, n=top_n, min_total=min_total)
|
| 56 |
-
if not rows:
|
| 57 |
-
return ""
|
| 58 |
-
labels = labels or {}
|
| 59 |
-
title = labels.get(
|
| 60 |
-
"lexmod_title", "Modernisation lexicale (top tokens)",
|
| 61 |
-
)
|
| 62 |
-
note = labels.get(
|
| 63 |
-
"lexmod_note",
|
| 64 |
-
"Tokens GT que le moteur réécrit le plus souvent. "
|
| 65 |
-
"Lecture : « maistre → maître modernisé dans 85 % des cas » "
|
| 66 |
-
"indique de quoi corriger dans le prompt pour préserver "
|
| 67 |
-
"l'orthographe historique.",
|
| 68 |
-
)
|
| 69 |
-
gt_label = labels.get("lexmod_gt_label", "Forme historique GT")
|
| 70 |
-
hyp_label = labels.get("lexmod_hyp_label", "Variantes OCR")
|
| 71 |
-
n_label = labels.get("lexmod_n_label", "n GT")
|
| 72 |
-
rate_label = labels.get("lexmod_rate_label", "% modernisé")
|
| 73 |
-
|
| 74 |
-
parts = [
|
| 75 |
-
'<div class="lexmod" style="margin:1rem 0">',
|
| 76 |
-
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 77 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 78 |
-
f'{_e(note)}</div>',
|
| 79 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 80 |
-
'font-size:.85rem">',
|
| 81 |
-
'<thead><tr>',
|
| 82 |
-
]
|
| 83 |
-
for col in (gt_label, hyp_label, n_label, rate_label):
|
| 84 |
-
parts.append(
|
| 85 |
-
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 86 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 87 |
-
f'{_e(col)}</th>'
|
| 88 |
-
)
|
| 89 |
-
parts.append("</tr></thead><tbody>")
|
| 90 |
-
for gt_token, slot in rows:
|
| 91 |
-
rate = slot.get("rate_modernized", 0.0)
|
| 92 |
-
n_total = slot.get("n_total", 0)
|
| 93 |
-
variants_str = _format_variants(slot.get("variants") or {})
|
| 94 |
-
rate_color = color_single_gradient(rate, end_rgb=GRADIENT_TARGET_ORANGE)
|
| 95 |
-
parts.append(
|
| 96 |
-
f'<tr>'
|
| 97 |
-
f'<td style="padding:.3rem .5rem;font-family:monospace">'
|
| 98 |
-
f'{_e(gt_token)}</td>'
|
| 99 |
-
f'<td style="padding:.3rem .5rem;font-size:.85rem">'
|
| 100 |
-
f'{variants_str}</td>'
|
| 101 |
-
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 102 |
-
f'font-family:monospace">{n_total}</td>'
|
| 103 |
-
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 104 |
-
f'background:{rate_color};font-family:monospace">'
|
| 105 |
-
f'{rate * 100:.0f}%</td>'
|
| 106 |
-
f'</tr>'
|
| 107 |
-
)
|
| 108 |
-
parts.append("</tbody></table></div>")
|
| 109 |
-
return "".join(parts)
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
__all__ = [
|
| 113 |
-
"build_lexical_modernization_html",
|
| 114 |
-
]
|
|
|
|
| 1 |
+
"""``picarones.report.lexical_modernization_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.lexical_modernization`.
|
| 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.lexical_modernization import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.lexical_modernization_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.lexical_modernization instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,165 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
injection systématique.
|
| 6 |
-
|
| 7 |
-
Vue
|
| 8 |
-
---
|
| 9 |
-
Tableau résumé moteur × {n_runs, premier CER, dernier CER,
|
| 10 |
-
variation cumulée colorée, pente annualisée, R², point de
|
| 11 |
-
rupture si détecté}.
|
| 12 |
-
|
| 13 |
-
Adaptive : ``""`` si la liste est vide.
|
| 14 |
-
|
| 15 |
-
Note d'intégration
|
| 16 |
-
------------------
|
| 17 |
-
Module pur — l'utilisateur compose :
|
| 18 |
-
|
| 19 |
-
.. code-block:: python
|
| 20 |
-
|
| 21 |
-
from picarones.measurements.history import BenchmarkHistory
|
| 22 |
-
from picarones.measurements.longitudinal import compute_corpus_longitudinal
|
| 23 |
-
from picarones.report.longitudinal_render import build_longitudinal_html
|
| 24 |
-
|
| 25 |
-
hist = BenchmarkHistory(db_path)
|
| 26 |
-
entries = hist.list_entries()
|
| 27 |
-
trends = compute_corpus_longitudinal(entries, corpus_name)
|
| 28 |
-
html = build_longitudinal_html(trends, labels)
|
| 29 |
"""
|
| 30 |
|
| 31 |
from __future__ import annotations
|
| 32 |
|
| 33 |
-
|
| 34 |
-
from typing import Optional
|
| 35 |
-
|
| 36 |
-
from picarones.reports_v2._helpers.render_helpers import color_diverging
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def _bg_for_cer_delta(delta_pct: float) -> str:
|
| 40 |
-
"""Cellule colorée pour un delta de CER en points de pourcentage :
|
| 41 |
-
vert si delta ≈ 0, orange/rouge en régression, bleu en amélioration.
|
| 42 |
-
Saturation à ±5 points.
|
| 43 |
-
"""
|
| 44 |
-
if abs(delta_pct) < 1.0:
|
| 45 |
-
return "#a7f0a7"
|
| 46 |
-
return color_diverging(
|
| 47 |
-
delta_pct,
|
| 48 |
-
max_abs=5.0,
|
| 49 |
-
neutral_rgb=(167, 240, 167),
|
| 50 |
-
positive_rgb=(220, 50, 50),
|
| 51 |
-
negative_rgb=(90, 160, 210),
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def build_longitudinal_html(
|
| 56 |
-
trends: Optional[list],
|
| 57 |
-
labels: Optional[dict[str, str]] = None,
|
| 58 |
-
) -> str:
|
| 59 |
-
"""Construit la vue HTML longitudinale.
|
| 60 |
-
|
| 61 |
-
Parameters
|
| 62 |
-
----------
|
| 63 |
-
trends:
|
| 64 |
-
Sortie de ``compute_corpus_longitudinal`` (liste de
|
| 65 |
-
dicts). Si ``None`` ou vide, retourne ``""``.
|
| 66 |
-
labels:
|
| 67 |
-
Dict i18n. Clés sous le préfixe ``longitudinal_*``.
|
| 68 |
-
"""
|
| 69 |
-
if not trends:
|
| 70 |
-
return ""
|
| 71 |
-
rows = [t for t in trends if isinstance(t, dict) and t.get("engine_name")]
|
| 72 |
-
if not rows:
|
| 73 |
-
return ""
|
| 74 |
-
labels = labels or {}
|
| 75 |
-
title = labels.get(
|
| 76 |
-
"longitudinal_title", "Évolution dans le temps",
|
| 77 |
-
)
|
| 78 |
-
note = labels.get(
|
| 79 |
-
"longitudinal_note",
|
| 80 |
-
"Tendance et points de rupture sur l'historique SQLite "
|
| 81 |
-
"des runs précédents. Une variation positive signale "
|
| 82 |
-
"une dégradation cumulée — utile pour relier une "
|
| 83 |
-
"régression à un changement de pipeline ou de modèle.",
|
| 84 |
-
)
|
| 85 |
-
h_engine = labels.get("longitudinal_engine", "Moteur")
|
| 86 |
-
h_n_runs = labels.get("longitudinal_n_runs", "Runs")
|
| 87 |
-
h_first = labels.get("longitudinal_first", "Premier CER")
|
| 88 |
-
h_last = labels.get("longitudinal_last", "Dernier CER")
|
| 89 |
-
h_delta = labels.get("longitudinal_delta", "Δ cumulé (pts)")
|
| 90 |
-
h_slope = labels.get("longitudinal_slope", "Pente annuelle (pts/an)")
|
| 91 |
-
h_r2 = labels.get("longitudinal_r2", "R²")
|
| 92 |
-
h_change = labels.get("longitudinal_change", "Rupture")
|
| 93 |
-
|
| 94 |
-
parts = [
|
| 95 |
-
'<section class="longitudinal-section" style="margin:1rem 0">',
|
| 96 |
-
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 97 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 98 |
-
f'{_e(note)}</div>',
|
| 99 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 100 |
-
'font-size:.9rem">',
|
| 101 |
-
'<thead><tr>',
|
| 102 |
-
]
|
| 103 |
-
for col in (h_engine, h_n_runs, h_first, h_last, h_delta,
|
| 104 |
-
h_slope, h_r2, h_change):
|
| 105 |
-
parts.append(
|
| 106 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 107 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 108 |
-
f'{_e(col)}</th>'
|
| 109 |
-
)
|
| 110 |
-
parts.append("</tr></thead><tbody>")
|
| 111 |
-
for entry in sorted(
|
| 112 |
-
rows,
|
| 113 |
-
key=lambda r: -float(r.get("absolute_delta") or 0.0),
|
| 114 |
-
):
|
| 115 |
-
engine = str(entry.get("engine_name") or "?")
|
| 116 |
-
n_runs = int(entry.get("n_runs") or 0)
|
| 117 |
-
first_cer = float(entry.get("first_cer") or 0.0)
|
| 118 |
-
last_cer = float(entry.get("last_cer") or 0.0)
|
| 119 |
-
delta_pct = float(entry.get("absolute_delta_pct") or 0.0)
|
| 120 |
-
delta_color = _bg_for_cer_delta(delta_pct)
|
| 121 |
-
trend = entry.get("trend") or {}
|
| 122 |
-
slope = trend.get("slope")
|
| 123 |
-
r2 = trend.get("r_squared")
|
| 124 |
-
slope_str = (
|
| 125 |
-
f"{float(slope) * 365 * 100:+.2f}"
|
| 126 |
-
if isinstance(slope, (int, float)) else "—"
|
| 127 |
-
)
|
| 128 |
-
r2_str = (
|
| 129 |
-
f"{float(r2):.2f}"
|
| 130 |
-
if isinstance(r2, (int, float)) else "—"
|
| 131 |
-
)
|
| 132 |
-
cp = entry.get("change_point")
|
| 133 |
-
if isinstance(cp, dict) and cp.get("timestamp"):
|
| 134 |
-
cp_delta = float(cp.get("delta") or 0.0)
|
| 135 |
-
cp_str = (
|
| 136 |
-
f'{_e(str(cp["timestamp"]))} '
|
| 137 |
-
f'<span style="opacity:.75">'
|
| 138 |
-
f'({cp_delta * 100:+.2f} pts)</span>'
|
| 139 |
-
)
|
| 140 |
-
else:
|
| 141 |
-
cp_str = "—"
|
| 142 |
-
parts.append(
|
| 143 |
-
f'<tr>'
|
| 144 |
-
f'<td style="padding:.4rem .6rem">{_e(engine)}</td>'
|
| 145 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 146 |
-
f'font-family:monospace">{n_runs}</td>'
|
| 147 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 148 |
-
f'font-family:monospace">{first_cer * 100:.2f}%</td>'
|
| 149 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 150 |
-
f'font-family:monospace">{last_cer * 100:.2f}%</td>'
|
| 151 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 152 |
-
f'background:{delta_color};font-family:monospace;'
|
| 153 |
-
f'font-weight:600">{delta_pct:+.2f}</td>'
|
| 154 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 155 |
-
f'font-family:monospace">{slope_str}</td>'
|
| 156 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 157 |
-
f'font-family:monospace">{r2_str}</td>'
|
| 158 |
-
f'<td style="padding:.4rem .6rem">{cp_str}</td>'
|
| 159 |
-
f'</tr>'
|
| 160 |
-
)
|
| 161 |
-
parts.append("</tbody></table></section>")
|
| 162 |
-
return "".join(parts)
|
| 163 |
|
|
|
|
| 164 |
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.longitudinal_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.longitudinal`.
|
| 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.longitudinal import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.longitudinal_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.longitudinal instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -1,151 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
pas de JS, anti-injection systématique.
|
| 6 |
-
|
| 7 |
-
Note d'intégration
|
| 8 |
-
------------------
|
| 9 |
-
La stabilité multi-runs n'est pas calculée automatiquement par
|
| 10 |
-
le runner — l'utilisateur doit relancer son moteur LLM/VLM
|
| 11 |
-
plusieurs fois (option ``--repeats N`` du runner reportée à un
|
| 12 |
-
sprint dédié) et appeler ``compute_multirun_stability`` lui-
|
| 13 |
-
même. Cette vue est donc un **module de rendu pur** que
|
| 14 |
-
l'utilisateur compose :
|
| 15 |
-
|
| 16 |
-
.. code-block:: python
|
| 17 |
-
|
| 18 |
-
from picarones.measurements.reliability import compute_multirun_stability
|
| 19 |
-
from picarones.report.multirun_stability_render import (
|
| 20 |
-
build_multirun_stability_html,
|
| 21 |
-
)
|
| 22 |
-
|
| 23 |
-
stability = []
|
| 24 |
-
for engine_name, runs in per_engine_runs.items():
|
| 25 |
-
s = compute_multirun_stability(runs, reference=ref)
|
| 26 |
-
if s is not None:
|
| 27 |
-
s["engine_name"] = engine_name
|
| 28 |
-
stability.append(s)
|
| 29 |
-
html = build_multirun_stability_html(stability, labels)
|
| 30 |
-
|
| 31 |
-
Vue
|
| 32 |
-
---
|
| 33 |
-
Tableau moteur × {n_runs, CER moyen ± écart-type, CV (%),
|
| 34 |
-
% paires identiques, n outputs distincts}. Cellule CV colorée
|
| 35 |
-
par gradient vert (stable) → rouge (instable, CV > 20 %).
|
| 36 |
-
|
| 37 |
-
Adaptive : ``""`` si la liste est vide ou que tous les
|
| 38 |
-
``cer_cv`` sont ``None``.
|
| 39 |
"""
|
| 40 |
|
| 41 |
from __future__ import annotations
|
| 42 |
|
| 43 |
-
|
| 44 |
-
from typing import Optional
|
| 45 |
-
|
| 46 |
-
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def build_multirun_stability_html(
|
| 50 |
-
stability: Optional[list],
|
| 51 |
-
labels: Optional[dict[str, str]] = None,
|
| 52 |
-
) -> str:
|
| 53 |
-
"""Construit la vue HTML de stabilité multi-runs.
|
| 54 |
-
|
| 55 |
-
Parameters
|
| 56 |
-
----------
|
| 57 |
-
stability:
|
| 58 |
-
Liste de dicts (un par moteur) issus de
|
| 59 |
-
``compute_multirun_stability`` enrichis d'un
|
| 60 |
-
``engine_name``. Si vide ou ``None``, retourne ``""``.
|
| 61 |
-
labels:
|
| 62 |
-
Dict i18n. Clés sous le préfixe ``stability_*``.
|
| 63 |
-
"""
|
| 64 |
-
if not stability:
|
| 65 |
-
return ""
|
| 66 |
-
rows = [s for s in stability if isinstance(s, dict) and s.get("engine_name")]
|
| 67 |
-
if not rows:
|
| 68 |
-
return ""
|
| 69 |
-
labels = labels or {}
|
| 70 |
-
title = labels.get("stability_title", "Stabilité multi-runs")
|
| 71 |
-
note = labels.get(
|
| 72 |
-
"stability_note",
|
| 73 |
-
"Quand un moteur LLM/VLM est non déterministe, la "
|
| 74 |
-
"variance entre runs successifs sur les mêmes documents "
|
| 75 |
-
"est un proxy de la fiabilité scientifique. Un CV élevé "
|
| 76 |
-
"ou un faible taux de runs identiques discrédite "
|
| 77 |
-
"l'interprétation du CER moyen.",
|
| 78 |
-
)
|
| 79 |
-
h_engine = labels.get("stability_engine", "Moteur")
|
| 80 |
-
h_n_runs = labels.get("stability_n_runs", "Runs")
|
| 81 |
-
h_cer = labels.get("stability_cer", "CER moyen ± σ")
|
| 82 |
-
h_cv = labels.get("stability_cv", "CV (%)")
|
| 83 |
-
h_identical = labels.get("stability_identical", "% runs identiques")
|
| 84 |
-
h_distinct = labels.get("stability_distinct", "Sorties distinctes")
|
| 85 |
-
|
| 86 |
-
parts = [
|
| 87 |
-
'<section class="stability-section" style="margin:1rem 0">',
|
| 88 |
-
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 89 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 90 |
-
f'{_e(note)}</div>',
|
| 91 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 92 |
-
'font-size:.9rem">',
|
| 93 |
-
'<thead><tr>',
|
| 94 |
-
]
|
| 95 |
-
for col in (h_engine, h_n_runs, h_cer, h_cv, h_identical, h_distinct):
|
| 96 |
-
parts.append(
|
| 97 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 98 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 99 |
-
f'{_e(col)}</th>'
|
| 100 |
-
)
|
| 101 |
-
parts.append("</tr></thead><tbody>")
|
| 102 |
-
for stab in rows:
|
| 103 |
-
engine = str(stab.get("engine_name") or "?")
|
| 104 |
-
n_runs = int(stab.get("n_runs") or 0)
|
| 105 |
-
cer_mean = stab.get("cer_mean")
|
| 106 |
-
cer_stdev = stab.get("cer_stdev")
|
| 107 |
-
cer_cv = stab.get("cer_cv")
|
| 108 |
-
identical = stab.get("identical_run_rate")
|
| 109 |
-
n_distinct = stab.get("n_distinct_outputs")
|
| 110 |
-
if isinstance(cer_mean, (int, float)) and isinstance(cer_stdev, (int, float)):
|
| 111 |
-
cer_str = f"{cer_mean * 100:.2f}% ± {cer_stdev * 100:.2f}%"
|
| 112 |
-
elif isinstance(cer_mean, (int, float)):
|
| 113 |
-
cer_str = f"{cer_mean * 100:.2f}%"
|
| 114 |
-
else:
|
| 115 |
-
cer_str = "—"
|
| 116 |
-
if isinstance(cer_cv, (int, float)):
|
| 117 |
-
cv_color = color_traffic_light(float(cer_cv), low_is_good=True, scale_max=0.25)
|
| 118 |
-
cv_cell = (
|
| 119 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 120 |
-
f'background:{cv_color};font-family:monospace;'
|
| 121 |
-
f'font-weight:600">{float(cer_cv) * 100:.1f}</td>'
|
| 122 |
-
)
|
| 123 |
-
else:
|
| 124 |
-
cv_cell = (
|
| 125 |
-
'<td style="padding:.4rem .6rem;text-align:right;'
|
| 126 |
-
'opacity:.4">—</td>'
|
| 127 |
-
)
|
| 128 |
-
identical_str = (
|
| 129 |
-
f"{float(identical) * 100:.1f}"
|
| 130 |
-
if isinstance(identical, (int, float)) else "—"
|
| 131 |
-
)
|
| 132 |
-
distinct_str = str(n_distinct) if isinstance(n_distinct, int) else "—"
|
| 133 |
-
parts.append(
|
| 134 |
-
f'<tr>'
|
| 135 |
-
f'<td style="padding:.4rem .6rem">{_e(engine)}</td>'
|
| 136 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 137 |
-
f'font-family:monospace">{n_runs}</td>'
|
| 138 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 139 |
-
f'font-family:monospace">{cer_str}</td>'
|
| 140 |
-
f'{cv_cell}'
|
| 141 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 142 |
-
f'font-family:monospace">{identical_str}</td>'
|
| 143 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 144 |
-
f'font-family:monospace">{distinct_str}</td>'
|
| 145 |
-
f'</tr>'
|
| 146 |
-
)
|
| 147 |
-
parts.append("</tbody></table></section>")
|
| 148 |
-
return "".join(parts)
|
| 149 |
|
|
|
|
| 150 |
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.multirun_stability_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.multirun_stability`.
|
| 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.multirun_stability import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.multirun_stability_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.multirun_stability instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -1,154 +1,18 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
injection systématique.
|
| 6 |
-
|
| 7 |
-
Vue
|
| 8 |
-
---
|
| 9 |
-
Tableau résumé moteur × {pages/h brut, pages/h **utilisable**,
|
| 10 |
-
% temps de correction (drag), n_pages, n_errors}. La cellule
|
| 11 |
-
**pages/h utilisable** est colorée en gradient rouge (faible)
|
| 12 |
-
→ vert (élevé), normalisé sur le maximum observé.
|
| 13 |
-
|
| 14 |
-
Adaptive : ``""`` si ``aggregate_effective_throughput`` retourne
|
| 15 |
-
``None`` (aucun moteur exploitable).
|
| 16 |
-
|
| 17 |
-
Note d'intégration
|
| 18 |
-
------------------
|
| 19 |
-
Cette vue est un **module pur** — l'utilisateur compose :
|
| 20 |
-
|
| 21 |
-
.. code-block:: python
|
| 22 |
-
|
| 23 |
-
from picarones.measurements.throughput import (
|
| 24 |
-
aggregate_effective_throughput,
|
| 25 |
-
)
|
| 26 |
-
from picarones.report.throughput_render import (
|
| 27 |
-
build_throughput_html,
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
per_engine = []
|
| 31 |
-
for report in benchmark.engine_reports:
|
| 32 |
-
n_errors = sum(
|
| 33 |
-
int(round(dr.metrics.wer * dr.metrics.reference_length / 5))
|
| 34 |
-
for dr in report.document_results
|
| 35 |
-
)
|
| 36 |
-
per_engine.append({
|
| 37 |
-
"engine_name": report.engine_name,
|
| 38 |
-
"n_pages": len(report.document_results),
|
| 39 |
-
"duration_seconds": sum(
|
| 40 |
-
dr.duration_seconds for dr in report.document_results
|
| 41 |
-
),
|
| 42 |
-
"n_errors": n_errors,
|
| 43 |
-
})
|
| 44 |
-
agg = aggregate_effective_throughput(per_engine)
|
| 45 |
-
html = build_throughput_html(agg, labels)
|
| 46 |
"""
|
| 47 |
|
| 48 |
from __future__ import annotations
|
| 49 |
|
| 50 |
-
|
| 51 |
-
from typing import Optional
|
| 52 |
-
|
| 53 |
-
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def build_throughput_html(
|
| 57 |
-
aggregated: Optional[dict],
|
| 58 |
-
labels: Optional[dict[str, str]] = None,
|
| 59 |
-
) -> str:
|
| 60 |
-
"""Construit la vue HTML throughput effectif.
|
| 61 |
-
|
| 62 |
-
Parameters
|
| 63 |
-
----------
|
| 64 |
-
aggregated:
|
| 65 |
-
Sortie de ``aggregate_effective_throughput``. Si
|
| 66 |
-
``None`` ou liste vide, retourne ``""``.
|
| 67 |
-
labels:
|
| 68 |
-
Dict i18n. Clés sous le préfixe ``throughput_*``.
|
| 69 |
-
"""
|
| 70 |
-
if not aggregated:
|
| 71 |
-
return ""
|
| 72 |
-
rows = aggregated.get("engines") or []
|
| 73 |
-
if not rows:
|
| 74 |
-
return ""
|
| 75 |
-
labels = labels or {}
|
| 76 |
-
title = labels.get("throughput_title", "Throughput effectif")
|
| 77 |
-
note = labels.get(
|
| 78 |
-
"throughput_note",
|
| 79 |
-
"Pages traitables par heure en intégrant le temps de "
|
| 80 |
-
"correction humaine post-OCR. Discrimine entre un cloud "
|
| 81 |
-
"rapide mais imprécis et un local lent mais fiable. "
|
| 82 |
-
"Constante de correction : {time_per_error}s par erreur "
|
| 83 |
-
"(défaut HTR-United, surchargeable).",
|
| 84 |
-
)
|
| 85 |
-
time_per_error = aggregated.get("time_per_error_seconds", 5.0)
|
| 86 |
-
note = note.replace("{time_per_error}", f"{time_per_error:.0f}")
|
| 87 |
-
h_engine = labels.get("throughput_engine", "Moteur")
|
| 88 |
-
h_raw = labels.get("throughput_raw", "Pages/h brut")
|
| 89 |
-
h_effective = labels.get(
|
| 90 |
-
"throughput_effective", "Pages/h utilisable",
|
| 91 |
-
)
|
| 92 |
-
h_drag = labels.get("throughput_drag", "% correction")
|
| 93 |
-
h_pages = labels.get("throughput_pages", "Pages")
|
| 94 |
-
h_errors = labels.get("throughput_errors", "Erreurs")
|
| 95 |
-
max_eff = max(
|
| 96 |
-
(r.get("pages_per_hour_effective") or 0.0) for r in rows
|
| 97 |
-
)
|
| 98 |
-
|
| 99 |
-
parts = [
|
| 100 |
-
'<section class="throughput-section" style="margin:1rem 0">',
|
| 101 |
-
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 102 |
-
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 103 |
-
f'{_e(note)}</div>',
|
| 104 |
-
'<table style="border-collapse:collapse;width:100%;'
|
| 105 |
-
'font-size:.9rem">',
|
| 106 |
-
'<thead><tr>',
|
| 107 |
-
]
|
| 108 |
-
for col in (h_engine, h_raw, h_effective, h_drag, h_pages, h_errors):
|
| 109 |
-
parts.append(
|
| 110 |
-
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 111 |
-
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 112 |
-
f'{_e(col)}</th>'
|
| 113 |
-
)
|
| 114 |
-
parts.append("</tr></thead><tbody>")
|
| 115 |
-
for row in sorted(
|
| 116 |
-
rows,
|
| 117 |
-
key=lambda r: -(r.get("pages_per_hour_effective") or 0.0),
|
| 118 |
-
):
|
| 119 |
-
engine = str(row.get("engine_name") or "?")
|
| 120 |
-
raw = row.get("pages_per_hour_raw")
|
| 121 |
-
eff = row.get("pages_per_hour_effective") or 0.0
|
| 122 |
-
drag = row.get("drag_ratio") or 0.0
|
| 123 |
-
n_pages = int(row.get("n_pages") or 0)
|
| 124 |
-
n_errors = int(row.get("n_errors") or 0)
|
| 125 |
-
eff_color = (
|
| 126 |
-
color_traffic_light(eff, scale_max=max_eff)
|
| 127 |
-
if max_eff > 0 else "#e0e0e0"
|
| 128 |
-
)
|
| 129 |
-
drag_color = color_traffic_light(drag, low_is_good=True)
|
| 130 |
-
raw_str = (
|
| 131 |
-
f"{raw:,.0f}" if isinstance(raw, (int, float)) else "—"
|
| 132 |
-
)
|
| 133 |
-
parts.append(
|
| 134 |
-
f'<tr>'
|
| 135 |
-
f'<td style="padding:.4rem .6rem">{_e(engine)}</td>'
|
| 136 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 137 |
-
f'font-family:monospace">{raw_str}</td>'
|
| 138 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 139 |
-
f'background:{eff_color};font-family:monospace;'
|
| 140 |
-
f'font-weight:600">{eff:,.0f}</td>'
|
| 141 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 142 |
-
f'background:{drag_color};font-family:monospace">'
|
| 143 |
-
f'{drag * 100:.1f}%</td>'
|
| 144 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 145 |
-
f'font-family:monospace">{n_pages}</td>'
|
| 146 |
-
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 147 |
-
f'font-family:monospace">{n_errors}</td>'
|
| 148 |
-
f'</tr>'
|
| 149 |
-
)
|
| 150 |
-
parts.append("</tbody></table></section>")
|
| 151 |
-
return "".join(parts)
|
| 152 |
|
|
|
|
| 153 |
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``picarones.report.throughput_render`` — shim re-export (déprécié, suppression 2.0).
|
| 2 |
|
| 3 |
+
Canonique : :mod:`picarones.reports_v2.html.renderers.throughput`.
|
| 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.throughput import * # noqa: F401, F403
|
| 12 |
|
| 13 |
+
warnings.warn(
|
| 14 |
+
"picarones.report.throughput_render is deprecated and will be removed in 2.0. "
|
| 15 |
+
"Import from picarones.reports_v2.html.renderers.throughput instead.",
|
| 16 |
+
DeprecationWarning,
|
| 17 |
+
stacklevel=2,
|
| 18 |
+
)
|
|
@@ -203,7 +203,7 @@ def build_advanced_taxonomy_view_html(
|
|
| 203 |
# Sous-section 4 : modernisation lexicale (opt-in)
|
| 204 |
if lexical_modernization:
|
| 205 |
try:
|
| 206 |
-
from picarones.
|
| 207 |
build_lexical_modernization_html,
|
| 208 |
)
|
| 209 |
html = build_lexical_modernization_html(
|
|
|
|
| 203 |
# Sous-section 4 : modernisation lexicale (opt-in)
|
| 204 |
if lexical_modernization:
|
| 205 |
try:
|
| 206 |
+
from picarones.reports_v2.html.renderers.lexical_modernization import (
|
| 207 |
build_lexical_modernization_html,
|
| 208 |
)
|
| 209 |
html = build_lexical_modernization_html(
|
|
@@ -165,7 +165,7 @@ def build_diagnostics_view_html(
|
|
| 165 |
# Sous-section 4 : évolution longitudinale (opt-in)
|
| 166 |
if longitudinal:
|
| 167 |
try:
|
| 168 |
-
from picarones.
|
| 169 |
build_longitudinal_html,
|
| 170 |
)
|
| 171 |
html = build_longitudinal_html(longitudinal, labels=labels)
|
|
@@ -185,7 +185,7 @@ def build_diagnostics_view_html(
|
|
| 185 |
# Sous-section 5 : stabilité multi-runs (opt-in)
|
| 186 |
if stability:
|
| 187 |
try:
|
| 188 |
-
from picarones.
|
| 189 |
build_multirun_stability_html,
|
| 190 |
)
|
| 191 |
html = build_multirun_stability_html(stability, labels=labels)
|
|
|
|
| 165 |
# Sous-section 4 : évolution longitudinale (opt-in)
|
| 166 |
if longitudinal:
|
| 167 |
try:
|
| 168 |
+
from picarones.reports_v2.html.renderers.longitudinal import (
|
| 169 |
build_longitudinal_html,
|
| 170 |
)
|
| 171 |
html = build_longitudinal_html(longitudinal, labels=labels)
|
|
|
|
| 185 |
# Sous-section 5 : stabilité multi-runs (opt-in)
|
| 186 |
if stability:
|
| 187 |
try:
|
| 188 |
+
from picarones.reports_v2.html.renderers.multirun_stability import (
|
| 189 |
build_multirun_stability_html,
|
| 190 |
)
|
| 191 |
html = build_multirun_stability_html(stability, labels=labels)
|
|
@@ -134,7 +134,7 @@ def build_economics_view_html(
|
|
| 134 |
from picarones.measurements.throughput import (
|
| 135 |
aggregate_effective_throughput,
|
| 136 |
)
|
| 137 |
-
from picarones.
|
| 138 |
build_throughput_html,
|
| 139 |
)
|
| 140 |
inputs = _estimate_engine_throughput_inputs(engine_reports)
|
|
|
|
| 134 |
from picarones.measurements.throughput import (
|
| 135 |
aggregate_effective_throughput,
|
| 136 |
)
|
| 137 |
+
from picarones.reports_v2.html.renderers.throughput import (
|
| 138 |
build_throughput_html,
|
| 139 |
)
|
| 140 |
inputs = _estimate_engine_throughput_inputs(engine_reports)
|
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Helpers de rendu pour le score de difficulté intrinsèque.
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.difficulty_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.difficulty``. Le chemin
|
| 6 |
+
legacy reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 7 |
+
suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Sprint A3 (item B-2 de l'audit institutional-readiness-2026-05) :
|
| 10 |
+
``difficulty_color`` vivait précédemment dans
|
| 11 |
+
``picarones/measurements/difficulty.py`` et y violait la règle
|
| 12 |
+
Cercle 2 → Cercle 3 par un import paresseux de
|
| 13 |
+
``picarones.report.colors``. La fonction est désormais placée à sa
|
| 14 |
+
juste place — Cercle 3, à côté de la palette qu'elle consomme — et
|
| 15 |
+
``measurements/difficulty.py`` ne contient plus que de la logique
|
| 16 |
+
purement numérique.
|
| 17 |
+
|
| 18 |
+
Le module pur ``picarones.measurements.difficulty`` reste utilisable
|
| 19 |
+
sans dépendance vers ``picarones.report``.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
from picarones.reports_v2._helpers.colors import (
|
| 25 |
+
COLOR_GREEN,
|
| 26 |
+
COLOR_ORANGE,
|
| 27 |
+
COLOR_RED,
|
| 28 |
+
COLOR_YELLOW,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def difficulty_color(score: float) -> str:
|
| 33 |
+
"""Retourne une couleur CSS pour un score de difficulté ∈ [0, 1].
|
| 34 |
+
|
| 35 |
+
Convention :
|
| 36 |
+
|
| 37 |
+
- score < 0.25 → vert (« facile »)
|
| 38 |
+
- score < 0.50 → jaune (« modéré »)
|
| 39 |
+
- score < 0.75 → orange (« difficile »)
|
| 40 |
+
- score ≥ 0.75 → rouge (« très difficile »)
|
| 41 |
+
|
| 42 |
+
Le label texte correspondant est produit par
|
| 43 |
+
:func:`picarones.measurements.difficulty.difficulty_label`.
|
| 44 |
+
"""
|
| 45 |
+
if score < 0.25:
|
| 46 |
+
return COLOR_GREEN
|
| 47 |
+
if score < 0.50:
|
| 48 |
+
return COLOR_YELLOW
|
| 49 |
+
if score < 0.75:
|
| 50 |
+
return COLOR_ORANGE
|
| 51 |
+
return COLOR_RED
|
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML de la vue « Modernisation lexicale » — Sprint 80.
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.lexical_modernization_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.lexical_modernization``.
|
| 6 |
+
Le chemin legacy reste disponible via un shim avec
|
| 7 |
+
``DeprecationWarning`` ; suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
A.I.7 du plan d'évolution 2026.
|
| 10 |
+
|
| 11 |
+
Suite directe ``picarones/core/lexical_modernization.py``.
|
| 12 |
+
Pattern identique aux autres rendus (Sprints 41/43/62/67/72/74/75/76/77) :
|
| 13 |
+
**server-side**, pas de JavaScript, anti-injection systématique.
|
| 14 |
+
|
| 15 |
+
Vue
|
| 16 |
+
---
|
| 17 |
+
Tableau trié par taux de modernisation décroissant : forme
|
| 18 |
+
historique GT → forme(s) modernisée(s), occurrences GT, %.
|
| 19 |
+
Couleur de cellule pour le %.
|
| 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.lexical_modernization import top_modernized_tokens
|
| 28 |
+
from picarones.reports_v2._helpers.render_helpers import (
|
| 29 |
+
GRADIENT_TARGET_ORANGE,
|
| 30 |
+
color_single_gradient,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _format_variants(variants: dict, max_show: int = 3) -> str:
|
| 35 |
+
"""Liste compacte des variants modernisés."""
|
| 36 |
+
items = sorted(variants.items(), key=lambda kv: -kv[1])
|
| 37 |
+
shown = items[:max_show]
|
| 38 |
+
rest = len(items) - max_show
|
| 39 |
+
parts = [
|
| 40 |
+
f"{_e(form)} ({count})"
|
| 41 |
+
for form, count in shown
|
| 42 |
+
]
|
| 43 |
+
if rest > 0:
|
| 44 |
+
parts.append(f"+{rest}")
|
| 45 |
+
return ", ".join(parts)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def build_lexical_modernization_html(
|
| 49 |
+
data: Optional[dict],
|
| 50 |
+
labels: Optional[dict[str, str]] = None,
|
| 51 |
+
*,
|
| 52 |
+
top_n: int = 20,
|
| 53 |
+
min_total: int = 1,
|
| 54 |
+
) -> str:
|
| 55 |
+
"""Construit la table HTML de modernisation lexicale.
|
| 56 |
+
|
| 57 |
+
Retourne ``""`` si ``data is None`` ou si aucun token modernisé.
|
| 58 |
+
"""
|
| 59 |
+
if not data:
|
| 60 |
+
return ""
|
| 61 |
+
rows = top_modernized_tokens(data, n=top_n, min_total=min_total)
|
| 62 |
+
if not rows:
|
| 63 |
+
return ""
|
| 64 |
+
labels = labels or {}
|
| 65 |
+
title = labels.get(
|
| 66 |
+
"lexmod_title", "Modernisation lexicale (top tokens)",
|
| 67 |
+
)
|
| 68 |
+
note = labels.get(
|
| 69 |
+
"lexmod_note",
|
| 70 |
+
"Tokens GT que le moteur réécrit le plus souvent. "
|
| 71 |
+
"Lecture : « maistre → maître modernisé dans 85 % des cas » "
|
| 72 |
+
"indique de quoi corriger dans le prompt pour préserver "
|
| 73 |
+
"l'orthographe historique.",
|
| 74 |
+
)
|
| 75 |
+
gt_label = labels.get("lexmod_gt_label", "Forme historique GT")
|
| 76 |
+
hyp_label = labels.get("lexmod_hyp_label", "Variantes OCR")
|
| 77 |
+
n_label = labels.get("lexmod_n_label", "n GT")
|
| 78 |
+
rate_label = labels.get("lexmod_rate_label", "% modernisé")
|
| 79 |
+
|
| 80 |
+
parts = [
|
| 81 |
+
'<div class="lexmod" style="margin:1rem 0">',
|
| 82 |
+
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 83 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 84 |
+
f'{_e(note)}</div>',
|
| 85 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 86 |
+
'font-size:.85rem">',
|
| 87 |
+
'<thead><tr>',
|
| 88 |
+
]
|
| 89 |
+
for col in (gt_label, hyp_label, n_label, rate_label):
|
| 90 |
+
parts.append(
|
| 91 |
+
f'<th scope=\"col\" style="padding:.3rem .5rem;text-align:left;'
|
| 92 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 93 |
+
f'{_e(col)}</th>'
|
| 94 |
+
)
|
| 95 |
+
parts.append("</tr></thead><tbody>")
|
| 96 |
+
for gt_token, slot in rows:
|
| 97 |
+
rate = slot.get("rate_modernized", 0.0)
|
| 98 |
+
n_total = slot.get("n_total", 0)
|
| 99 |
+
variants_str = _format_variants(slot.get("variants") or {})
|
| 100 |
+
rate_color = color_single_gradient(rate, end_rgb=GRADIENT_TARGET_ORANGE)
|
| 101 |
+
parts.append(
|
| 102 |
+
f'<tr>'
|
| 103 |
+
f'<td style="padding:.3rem .5rem;font-family:monospace">'
|
| 104 |
+
f'{_e(gt_token)}</td>'
|
| 105 |
+
f'<td style="padding:.3rem .5rem;font-size:.85rem">'
|
| 106 |
+
f'{variants_str}</td>'
|
| 107 |
+
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 108 |
+
f'font-family:monospace">{n_total}</td>'
|
| 109 |
+
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 110 |
+
f'background:{rate_color};font-family:monospace">'
|
| 111 |
+
f'{rate * 100:.0f}%</td>'
|
| 112 |
+
f'</tr>'
|
| 113 |
+
)
|
| 114 |
+
parts.append("</tbody></table></div>")
|
| 115 |
+
return "".join(parts)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
__all__ = [
|
| 119 |
+
"build_lexical_modernization_html",
|
| 120 |
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML « Évolution dans le temps » — Sprint 92 (A.II.9).
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.longitudinal_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.longitudinal``. Le chemin
|
| 6 |
+
legacy reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 7 |
+
suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Suite directe ``picarones/core/longitudinal.py``. Pattern
|
| 10 |
+
identique aux autres rendus : server-side, pas de JS, anti-
|
| 11 |
+
injection systématique.
|
| 12 |
+
|
| 13 |
+
Vue
|
| 14 |
+
---
|
| 15 |
+
Tableau résumé moteur × {n_runs, premier CER, dernier CER,
|
| 16 |
+
variation cumulée colorée, pente annualisée, R², point de
|
| 17 |
+
rupture si détecté}.
|
| 18 |
+
|
| 19 |
+
Adaptive : ``""`` si la liste est vide.
|
| 20 |
+
|
| 21 |
+
Note d'intégration
|
| 22 |
+
------------------
|
| 23 |
+
Module pur — l'utilisateur compose :
|
| 24 |
+
|
| 25 |
+
.. code-block:: python
|
| 26 |
+
|
| 27 |
+
from picarones.measurements.history import BenchmarkHistory
|
| 28 |
+
from picarones.measurements.longitudinal import compute_corpus_longitudinal
|
| 29 |
+
from picarones.reports_v2.html.renderers.longitudinal import build_longitudinal_html
|
| 30 |
+
|
| 31 |
+
hist = BenchmarkHistory(db_path)
|
| 32 |
+
entries = hist.list_entries()
|
| 33 |
+
trends = compute_corpus_longitudinal(entries, corpus_name)
|
| 34 |
+
html = build_longitudinal_html(trends, labels)
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
from __future__ import annotations
|
| 38 |
+
|
| 39 |
+
from html import escape as _e
|
| 40 |
+
from typing import Optional
|
| 41 |
+
|
| 42 |
+
from picarones.reports_v2._helpers.render_helpers import color_diverging
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _bg_for_cer_delta(delta_pct: float) -> str:
|
| 46 |
+
"""Cellule colorée pour un delta de CER en points de pourcentage :
|
| 47 |
+
vert si delta ≈ 0, orange/rouge en régression, bleu en amélioration.
|
| 48 |
+
Saturation à ±5 points.
|
| 49 |
+
"""
|
| 50 |
+
if abs(delta_pct) < 1.0:
|
| 51 |
+
return "#a7f0a7"
|
| 52 |
+
return color_diverging(
|
| 53 |
+
delta_pct,
|
| 54 |
+
max_abs=5.0,
|
| 55 |
+
neutral_rgb=(167, 240, 167),
|
| 56 |
+
positive_rgb=(220, 50, 50),
|
| 57 |
+
negative_rgb=(90, 160, 210),
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def build_longitudinal_html(
|
| 62 |
+
trends: Optional[list],
|
| 63 |
+
labels: Optional[dict[str, str]] = None,
|
| 64 |
+
) -> str:
|
| 65 |
+
"""Construit la vue HTML longitudinale.
|
| 66 |
+
|
| 67 |
+
Parameters
|
| 68 |
+
----------
|
| 69 |
+
trends:
|
| 70 |
+
Sortie de ``compute_corpus_longitudinal`` (liste de
|
| 71 |
+
dicts). Si ``None`` ou vide, retourne ``""``.
|
| 72 |
+
labels:
|
| 73 |
+
Dict i18n. Clés sous le préfixe ``longitudinal_*``.
|
| 74 |
+
"""
|
| 75 |
+
if not trends:
|
| 76 |
+
return ""
|
| 77 |
+
rows = [t for t in trends if isinstance(t, dict) and t.get("engine_name")]
|
| 78 |
+
if not rows:
|
| 79 |
+
return ""
|
| 80 |
+
labels = labels or {}
|
| 81 |
+
title = labels.get(
|
| 82 |
+
"longitudinal_title", "Évolution dans le temps",
|
| 83 |
+
)
|
| 84 |
+
note = labels.get(
|
| 85 |
+
"longitudinal_note",
|
| 86 |
+
"Tendance et points de rupture sur l'historique SQLite "
|
| 87 |
+
"des runs précédents. Une variation positive signale "
|
| 88 |
+
"une dégradation cumulée — utile pour relier une "
|
| 89 |
+
"régression à un changement de pipeline ou de modèle.",
|
| 90 |
+
)
|
| 91 |
+
h_engine = labels.get("longitudinal_engine", "Moteur")
|
| 92 |
+
h_n_runs = labels.get("longitudinal_n_runs", "Runs")
|
| 93 |
+
h_first = labels.get("longitudinal_first", "Premier CER")
|
| 94 |
+
h_last = labels.get("longitudinal_last", "Dernier CER")
|
| 95 |
+
h_delta = labels.get("longitudinal_delta", "Δ cumulé (pts)")
|
| 96 |
+
h_slope = labels.get("longitudinal_slope", "Pente annuelle (pts/an)")
|
| 97 |
+
h_r2 = labels.get("longitudinal_r2", "R²")
|
| 98 |
+
h_change = labels.get("longitudinal_change", "Rupture")
|
| 99 |
+
|
| 100 |
+
parts = [
|
| 101 |
+
'<section class="longitudinal-section" style="margin:1rem 0">',
|
| 102 |
+
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 103 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 104 |
+
f'{_e(note)}</div>',
|
| 105 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 106 |
+
'font-size:.9rem">',
|
| 107 |
+
'<thead><tr>',
|
| 108 |
+
]
|
| 109 |
+
for col in (h_engine, h_n_runs, h_first, h_last, h_delta,
|
| 110 |
+
h_slope, h_r2, h_change):
|
| 111 |
+
parts.append(
|
| 112 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 113 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 114 |
+
f'{_e(col)}</th>'
|
| 115 |
+
)
|
| 116 |
+
parts.append("</tr></thead><tbody>")
|
| 117 |
+
for entry in sorted(
|
| 118 |
+
rows,
|
| 119 |
+
key=lambda r: -float(r.get("absolute_delta") or 0.0),
|
| 120 |
+
):
|
| 121 |
+
engine = str(entry.get("engine_name") or "?")
|
| 122 |
+
n_runs = int(entry.get("n_runs") or 0)
|
| 123 |
+
first_cer = float(entry.get("first_cer") or 0.0)
|
| 124 |
+
last_cer = float(entry.get("last_cer") or 0.0)
|
| 125 |
+
delta_pct = float(entry.get("absolute_delta_pct") or 0.0)
|
| 126 |
+
delta_color = _bg_for_cer_delta(delta_pct)
|
| 127 |
+
trend = entry.get("trend") or {}
|
| 128 |
+
slope = trend.get("slope")
|
| 129 |
+
r2 = trend.get("r_squared")
|
| 130 |
+
slope_str = (
|
| 131 |
+
f"{float(slope) * 365 * 100:+.2f}"
|
| 132 |
+
if isinstance(slope, (int, float)) else "—"
|
| 133 |
+
)
|
| 134 |
+
r2_str = (
|
| 135 |
+
f"{float(r2):.2f}"
|
| 136 |
+
if isinstance(r2, (int, float)) else "—"
|
| 137 |
+
)
|
| 138 |
+
cp = entry.get("change_point")
|
| 139 |
+
if isinstance(cp, dict) and cp.get("timestamp"):
|
| 140 |
+
cp_delta = float(cp.get("delta") or 0.0)
|
| 141 |
+
cp_str = (
|
| 142 |
+
f'{_e(str(cp["timestamp"]))} '
|
| 143 |
+
f'<span style="opacity:.75">'
|
| 144 |
+
f'({cp_delta * 100:+.2f} pts)</span>'
|
| 145 |
+
)
|
| 146 |
+
else:
|
| 147 |
+
cp_str = "—"
|
| 148 |
+
parts.append(
|
| 149 |
+
f'<tr>'
|
| 150 |
+
f'<td style="padding:.4rem .6rem">{_e(engine)}</td>'
|
| 151 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 152 |
+
f'font-family:monospace">{n_runs}</td>'
|
| 153 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 154 |
+
f'font-family:monospace">{first_cer * 100:.2f}%</td>'
|
| 155 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 156 |
+
f'font-family:monospace">{last_cer * 100:.2f}%</td>'
|
| 157 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 158 |
+
f'background:{delta_color};font-family:monospace;'
|
| 159 |
+
f'font-weight:600">{delta_pct:+.2f}</td>'
|
| 160 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 161 |
+
f'font-family:monospace">{slope_str}</td>'
|
| 162 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 163 |
+
f'font-family:monospace">{r2_str}</td>'
|
| 164 |
+
f'<td style="padding:.4rem .6rem">{cp_str}</td>'
|
| 165 |
+
f'</tr>'
|
| 166 |
+
)
|
| 167 |
+
parts.append("</tbody></table></section>")
|
| 168 |
+
return "".join(parts)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
__all__ = ["build_longitudinal_html"]
|
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML « Stabilité multi-runs » — Sprint 90 (A.II.4).
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.multirun_stability_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.multirun_stability``.
|
| 6 |
+
Le chemin legacy reste disponible via un shim avec
|
| 7 |
+
``DeprecationWarning`` ; suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Suite directe ``picarones/core/reliability.compute_multirun_stability``
|
| 10 |
+
(Sprint 83). Pattern identique aux autres rendus : server-side,
|
| 11 |
+
pas de JS, anti-injection systématique.
|
| 12 |
+
|
| 13 |
+
Note d'intégration
|
| 14 |
+
------------------
|
| 15 |
+
La stabilité multi-runs n'est pas calculée automatiquement par
|
| 16 |
+
le runner — l'utilisateur doit relancer son moteur LLM/VLM
|
| 17 |
+
plusieurs fois (option ``--repeats N`` du runner reportée à un
|
| 18 |
+
sprint dédié) et appeler ``compute_multirun_stability`` lui-
|
| 19 |
+
même. Cette vue est donc un **module de rendu pur** que
|
| 20 |
+
l'utilisateur compose :
|
| 21 |
+
|
| 22 |
+
.. code-block:: python
|
| 23 |
+
|
| 24 |
+
from picarones.measurements.reliability import compute_multirun_stability
|
| 25 |
+
from picarones.reports_v2.html.renderers.multirun_stability import (
|
| 26 |
+
build_multirun_stability_html,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
stability = []
|
| 30 |
+
for engine_name, runs in per_engine_runs.items():
|
| 31 |
+
s = compute_multirun_stability(runs, reference=ref)
|
| 32 |
+
if s is not None:
|
| 33 |
+
s["engine_name"] = engine_name
|
| 34 |
+
stability.append(s)
|
| 35 |
+
html = build_multirun_stability_html(stability, labels)
|
| 36 |
+
|
| 37 |
+
Vue
|
| 38 |
+
---
|
| 39 |
+
Tableau moteur × {n_runs, CER moyen ± écart-type, CV (%),
|
| 40 |
+
% paires identiques, n outputs distincts}. Cellule CV colorée
|
| 41 |
+
par gradient vert (stable) → rouge (instable, CV > 20 %).
|
| 42 |
+
|
| 43 |
+
Adaptive : ``""`` si la liste est vide ou que tous les
|
| 44 |
+
``cer_cv`` sont ``None``.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
from __future__ import annotations
|
| 48 |
+
|
| 49 |
+
from html import escape as _e
|
| 50 |
+
from typing import Optional
|
| 51 |
+
|
| 52 |
+
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def build_multirun_stability_html(
|
| 56 |
+
stability: Optional[list],
|
| 57 |
+
labels: Optional[dict[str, str]] = None,
|
| 58 |
+
) -> str:
|
| 59 |
+
"""Construit la vue HTML de stabilité multi-runs.
|
| 60 |
+
|
| 61 |
+
Parameters
|
| 62 |
+
----------
|
| 63 |
+
stability:
|
| 64 |
+
Liste de dicts (un par moteur) issus de
|
| 65 |
+
``compute_multirun_stability`` enrichis d'un
|
| 66 |
+
``engine_name``. Si vide ou ``None``, retourne ``""``.
|
| 67 |
+
labels:
|
| 68 |
+
Dict i18n. Clés sous le préfixe ``stability_*``.
|
| 69 |
+
"""
|
| 70 |
+
if not stability:
|
| 71 |
+
return ""
|
| 72 |
+
rows = [s for s in stability if isinstance(s, dict) and s.get("engine_name")]
|
| 73 |
+
if not rows:
|
| 74 |
+
return ""
|
| 75 |
+
labels = labels or {}
|
| 76 |
+
title = labels.get("stability_title", "Stabilité multi-runs")
|
| 77 |
+
note = labels.get(
|
| 78 |
+
"stability_note",
|
| 79 |
+
"Quand un moteur LLM/VLM est non déterministe, la "
|
| 80 |
+
"variance entre runs successifs sur les mêmes documents "
|
| 81 |
+
"est un proxy de la fiabilité scientifique. Un CV élevé "
|
| 82 |
+
"ou un faible taux de runs identiques discrédite "
|
| 83 |
+
"l'interprétation du CER moyen.",
|
| 84 |
+
)
|
| 85 |
+
h_engine = labels.get("stability_engine", "Moteur")
|
| 86 |
+
h_n_runs = labels.get("stability_n_runs", "Runs")
|
| 87 |
+
h_cer = labels.get("stability_cer", "CER moyen ± σ")
|
| 88 |
+
h_cv = labels.get("stability_cv", "CV (%)")
|
| 89 |
+
h_identical = labels.get("stability_identical", "% runs identiques")
|
| 90 |
+
h_distinct = labels.get("stability_distinct", "Sorties distinctes")
|
| 91 |
+
|
| 92 |
+
parts = [
|
| 93 |
+
'<section class="stability-section" style="margin:1rem 0">',
|
| 94 |
+
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 95 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 96 |
+
f'{_e(note)}</div>',
|
| 97 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 98 |
+
'font-size:.9rem">',
|
| 99 |
+
'<thead><tr>',
|
| 100 |
+
]
|
| 101 |
+
for col in (h_engine, h_n_runs, h_cer, h_cv, h_identical, h_distinct):
|
| 102 |
+
parts.append(
|
| 103 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 104 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 105 |
+
f'{_e(col)}</th>'
|
| 106 |
+
)
|
| 107 |
+
parts.append("</tr></thead><tbody>")
|
| 108 |
+
for stab in rows:
|
| 109 |
+
engine = str(stab.get("engine_name") or "?")
|
| 110 |
+
n_runs = int(stab.get("n_runs") or 0)
|
| 111 |
+
cer_mean = stab.get("cer_mean")
|
| 112 |
+
cer_stdev = stab.get("cer_stdev")
|
| 113 |
+
cer_cv = stab.get("cer_cv")
|
| 114 |
+
identical = stab.get("identical_run_rate")
|
| 115 |
+
n_distinct = stab.get("n_distinct_outputs")
|
| 116 |
+
if isinstance(cer_mean, (int, float)) and isinstance(cer_stdev, (int, float)):
|
| 117 |
+
cer_str = f"{cer_mean * 100:.2f}% ± {cer_stdev * 100:.2f}%"
|
| 118 |
+
elif isinstance(cer_mean, (int, float)):
|
| 119 |
+
cer_str = f"{cer_mean * 100:.2f}%"
|
| 120 |
+
else:
|
| 121 |
+
cer_str = "—"
|
| 122 |
+
if isinstance(cer_cv, (int, float)):
|
| 123 |
+
cv_color = color_traffic_light(float(cer_cv), low_is_good=True, scale_max=0.25)
|
| 124 |
+
cv_cell = (
|
| 125 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 126 |
+
f'background:{cv_color};font-family:monospace;'
|
| 127 |
+
f'font-weight:600">{float(cer_cv) * 100:.1f}</td>'
|
| 128 |
+
)
|
| 129 |
+
else:
|
| 130 |
+
cv_cell = (
|
| 131 |
+
'<td style="padding:.4rem .6rem;text-align:right;'
|
| 132 |
+
'opacity:.4">—</td>'
|
| 133 |
+
)
|
| 134 |
+
identical_str = (
|
| 135 |
+
f"{float(identical) * 100:.1f}"
|
| 136 |
+
if isinstance(identical, (int, float)) else "—"
|
| 137 |
+
)
|
| 138 |
+
distinct_str = str(n_distinct) if isinstance(n_distinct, int) else "—"
|
| 139 |
+
parts.append(
|
| 140 |
+
f'<tr>'
|
| 141 |
+
f'<td style="padding:.4rem .6rem">{_e(engine)}</td>'
|
| 142 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 143 |
+
f'font-family:monospace">{n_runs}</td>'
|
| 144 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 145 |
+
f'font-family:monospace">{cer_str}</td>'
|
| 146 |
+
f'{cv_cell}'
|
| 147 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 148 |
+
f'font-family:monospace">{identical_str}</td>'
|
| 149 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 150 |
+
f'font-family:monospace">{distinct_str}</td>'
|
| 151 |
+
f'</tr>'
|
| 152 |
+
)
|
| 153 |
+
parts.append("</tbody></table></section>")
|
| 154 |
+
return "".join(parts)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
__all__ = ["build_multirun_stability_html"]
|
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML « Throughput effectif » — Sprint 91 (A.II.6).
|
| 2 |
+
|
| 3 |
+
Phase 5.C — module relocalisé depuis
|
| 4 |
+
``picarones.report.throughput_render`` vers
|
| 5 |
+
``picarones.reports_v2.html.renderers.throughput``. Le chemin
|
| 6 |
+
legacy reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 7 |
+
suppression prévue en 2.0.
|
| 8 |
+
|
| 9 |
+
Suite directe ``picarones/core/throughput.py``. Pattern
|
| 10 |
+
identique aux autres rendus : server-side, pas de JS, anti-
|
| 11 |
+
injection systématique.
|
| 12 |
+
|
| 13 |
+
Vue
|
| 14 |
+
---
|
| 15 |
+
Tableau résumé moteur × {pages/h brut, pages/h **utilisable**,
|
| 16 |
+
% temps de correction (drag), n_pages, n_errors}. La cellule
|
| 17 |
+
**pages/h utilisable** est colorée en gradient rouge (faible)
|
| 18 |
+
→ vert (élevé), normalisé sur le maximum observé.
|
| 19 |
+
|
| 20 |
+
Adaptive : ``""`` si ``aggregate_effective_throughput`` retourne
|
| 21 |
+
``None`` (aucun moteur exploitable).
|
| 22 |
+
|
| 23 |
+
Note d'intégration
|
| 24 |
+
------------------
|
| 25 |
+
Cette vue est un **module pur** — l'utilisateur compose :
|
| 26 |
+
|
| 27 |
+
.. code-block:: python
|
| 28 |
+
|
| 29 |
+
from picarones.measurements.throughput import (
|
| 30 |
+
aggregate_effective_throughput,
|
| 31 |
+
)
|
| 32 |
+
from picarones.reports_v2.html.renderers.throughput import (
|
| 33 |
+
build_throughput_html,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
per_engine = []
|
| 37 |
+
for report in benchmark.engine_reports:
|
| 38 |
+
n_errors = sum(
|
| 39 |
+
int(round(dr.metrics.wer * dr.metrics.reference_length / 5))
|
| 40 |
+
for dr in report.document_results
|
| 41 |
+
)
|
| 42 |
+
per_engine.append({
|
| 43 |
+
"engine_name": report.engine_name,
|
| 44 |
+
"n_pages": len(report.document_results),
|
| 45 |
+
"duration_seconds": sum(
|
| 46 |
+
dr.duration_seconds for dr in report.document_results
|
| 47 |
+
),
|
| 48 |
+
"n_errors": n_errors,
|
| 49 |
+
})
|
| 50 |
+
agg = aggregate_effective_throughput(per_engine)
|
| 51 |
+
html = build_throughput_html(agg, labels)
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
from __future__ import annotations
|
| 55 |
+
|
| 56 |
+
from html import escape as _e
|
| 57 |
+
from typing import Optional
|
| 58 |
+
|
| 59 |
+
from picarones.reports_v2._helpers.render_helpers import color_traffic_light
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def build_throughput_html(
|
| 63 |
+
aggregated: Optional[dict],
|
| 64 |
+
labels: Optional[dict[str, str]] = None,
|
| 65 |
+
) -> str:
|
| 66 |
+
"""Construit la vue HTML throughput effectif.
|
| 67 |
+
|
| 68 |
+
Parameters
|
| 69 |
+
----------
|
| 70 |
+
aggregated:
|
| 71 |
+
Sortie de ``aggregate_effective_throughput``. Si
|
| 72 |
+
``None`` ou liste vide, retourne ``""``.
|
| 73 |
+
labels:
|
| 74 |
+
Dict i18n. Clés sous le préfixe ``throughput_*``.
|
| 75 |
+
"""
|
| 76 |
+
if not aggregated:
|
| 77 |
+
return ""
|
| 78 |
+
rows = aggregated.get("engines") or []
|
| 79 |
+
if not rows:
|
| 80 |
+
return ""
|
| 81 |
+
labels = labels or {}
|
| 82 |
+
title = labels.get("throughput_title", "Throughput effectif")
|
| 83 |
+
note = labels.get(
|
| 84 |
+
"throughput_note",
|
| 85 |
+
"Pages traitables par heure en intégrant le temps de "
|
| 86 |
+
"correction humaine post-OCR. Discrimine entre un cloud "
|
| 87 |
+
"rapide mais imprécis et un local lent mais fiable. "
|
| 88 |
+
"Constante de correction : {time_per_error}s par erreur "
|
| 89 |
+
"(défaut HTR-United, surchargeable).",
|
| 90 |
+
)
|
| 91 |
+
time_per_error = aggregated.get("time_per_error_seconds", 5.0)
|
| 92 |
+
note = note.replace("{time_per_error}", f"{time_per_error:.0f}")
|
| 93 |
+
h_engine = labels.get("throughput_engine", "Moteur")
|
| 94 |
+
h_raw = labels.get("throughput_raw", "Pages/h brut")
|
| 95 |
+
h_effective = labels.get(
|
| 96 |
+
"throughput_effective", "Pages/h utilisable",
|
| 97 |
+
)
|
| 98 |
+
h_drag = labels.get("throughput_drag", "% correction")
|
| 99 |
+
h_pages = labels.get("throughput_pages", "Pages")
|
| 100 |
+
h_errors = labels.get("throughput_errors", "Erreurs")
|
| 101 |
+
max_eff = max(
|
| 102 |
+
(r.get("pages_per_hour_effective") or 0.0) for r in rows
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
parts = [
|
| 106 |
+
'<section class="throughput-section" style="margin:1rem 0">',
|
| 107 |
+
f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>',
|
| 108 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.6rem">'
|
| 109 |
+
f'{_e(note)}</div>',
|
| 110 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 111 |
+
'font-size:.9rem">',
|
| 112 |
+
'<thead><tr>',
|
| 113 |
+
]
|
| 114 |
+
for col in (h_engine, h_raw, h_effective, h_drag, h_pages, h_errors):
|
| 115 |
+
parts.append(
|
| 116 |
+
f'<th scope=\"col\" style="padding:.4rem .6rem;text-align:left;'
|
| 117 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 118 |
+
f'{_e(col)}</th>'
|
| 119 |
+
)
|
| 120 |
+
parts.append("</tr></thead><tbody>")
|
| 121 |
+
for row in sorted(
|
| 122 |
+
rows,
|
| 123 |
+
key=lambda r: -(r.get("pages_per_hour_effective") or 0.0),
|
| 124 |
+
):
|
| 125 |
+
engine = str(row.get("engine_name") or "?")
|
| 126 |
+
raw = row.get("pages_per_hour_raw")
|
| 127 |
+
eff = row.get("pages_per_hour_effective") or 0.0
|
| 128 |
+
drag = row.get("drag_ratio") or 0.0
|
| 129 |
+
n_pages = int(row.get("n_pages") or 0)
|
| 130 |
+
n_errors = int(row.get("n_errors") or 0)
|
| 131 |
+
eff_color = (
|
| 132 |
+
color_traffic_light(eff, scale_max=max_eff)
|
| 133 |
+
if max_eff > 0 else "#e0e0e0"
|
| 134 |
+
)
|
| 135 |
+
drag_color = color_traffic_light(drag, low_is_good=True)
|
| 136 |
+
raw_str = (
|
| 137 |
+
f"{raw:,.0f}" if isinstance(raw, (int, float)) else "—"
|
| 138 |
+
)
|
| 139 |
+
parts.append(
|
| 140 |
+
f'<tr>'
|
| 141 |
+
f'<td style="padding:.4rem .6rem">{_e(engine)}</td>'
|
| 142 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 143 |
+
f'font-family:monospace">{raw_str}</td>'
|
| 144 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 145 |
+
f'background:{eff_color};font-family:monospace;'
|
| 146 |
+
f'font-weight:600">{eff:,.0f}</td>'
|
| 147 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 148 |
+
f'background:{drag_color};font-family:monospace">'
|
| 149 |
+
f'{drag * 100:.1f}%</td>'
|
| 150 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 151 |
+
f'font-family:monospace">{n_pages}</td>'
|
| 152 |
+
f'<td style="padding:.4rem .6rem;text-align:right;'
|
| 153 |
+
f'font-family:monospace">{n_errors}</td>'
|
| 154 |
+
f'</tr>'
|
| 155 |
+
)
|
| 156 |
+
parts.append("</tbody></table></section>")
|
| 157 |
+
return "".join(parts)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
__all__ = ["build_throughput_html"]
|
|
@@ -61,17 +61,17 @@ MEASUREMENTS_DIR = PICARONES_DIR / "measurements"
|
|
| 61 |
#: de fin de ligne ``noqa F401`` dans
|
| 62 |
#: ``picarones/measurements/__init__.py``).
|
| 63 |
TEST_ONLY_BASELINE: frozenset[str] = frozenset({
|
| 64 |
-
# Phase 5.C : ``measurements/
|
| 65 |
-
# vers ``evaluation/metrics/
|
| 66 |
-
#
|
| 67 |
-
# ``
|
| 68 |
-
#
|
| 69 |
-
#
|
| 70 |
-
#
|
| 71 |
-
#
|
| 72 |
-
#
|
| 73 |
-
# au plus tard à la version 2.0 quand le shim disparaîtra.
|
| 74 |
"specialization",
|
|
|
|
| 75 |
})
|
| 76 |
|
| 77 |
|
|
|
|
| 61 |
#: de fin de ligne ``noqa F401`` dans
|
| 62 |
#: ``picarones/measurements/__init__.py``).
|
| 63 |
TEST_ONLY_BASELINE: frozenset[str] = frozenset({
|
| 64 |
+
# Phase 5.C : modules ``measurements/X.py`` qui sont des shims
|
| 65 |
+
# vers ``evaluation/metrics/X``. Leur unique consommateur
|
| 66 |
+
# production (le renderer ``report/X_render.py``) a été migré
|
| 67 |
+
# vers ``reports_v2/html/renderers/X.py`` qui importe le
|
| 68 |
+
# canonique directement (la règle layer-dependencies interdit
|
| 69 |
+
# d'importer un shim depuis ``reports_v2/``). Tant que des
|
| 70 |
+
# tests ou un caller externe utilisent le chemin legacy, le
|
| 71 |
+
# shim reste en place ; les entrées ici sont retirées au plus
|
| 72 |
+
# tard à la version 2.0 quand les shims disparaîtront.
|
|
|
|
| 73 |
"specialization",
|
| 74 |
+
"lexical_modernization",
|
| 75 |
})
|
| 76 |
|
| 77 |
|
|
@@ -32,7 +32,7 @@ from picarones.measurements.lexical_modernization import (
|
|
| 32 |
compute_lexical_modernization,
|
| 33 |
top_modernized_tokens,
|
| 34 |
)
|
| 35 |
-
from picarones.
|
| 36 |
build_lexical_modernization_html,
|
| 37 |
)
|
| 38 |
|
|
|
|
| 32 |
compute_lexical_modernization,
|
| 33 |
top_modernized_tokens,
|
| 34 |
)
|
| 35 |
+
from picarones.reports_v2.html.renderers.lexical_modernization import (
|
| 36 |
build_lexical_modernization_html,
|
| 37 |
)
|
| 38 |
|
|
@@ -24,7 +24,7 @@ from pathlib import Path
|
|
| 24 |
from picarones.measurements.narrative import build_synthesis
|
| 25 |
from picarones.measurements.narrative.detectors import detect_engine_unstable
|
| 26 |
from picarones.core.facts import FactImportance, FactType
|
| 27 |
-
from picarones.
|
| 28 |
build_multirun_stability_html,
|
| 29 |
)
|
| 30 |
|
|
|
|
| 24 |
from picarones.measurements.narrative import build_synthesis
|
| 25 |
from picarones.measurements.narrative.detectors import detect_engine_unstable
|
| 26 |
from picarones.core.facts import FactImportance, FactType
|
| 27 |
+
from picarones.reports_v2.html.renderers.multirun_stability import (
|
| 28 |
build_multirun_stability_html,
|
| 29 |
)
|
| 30 |
|
|
@@ -28,7 +28,7 @@ from picarones.measurements.throughput import (
|
|
| 28 |
aggregate_effective_throughput,
|
| 29 |
compute_effective_throughput,
|
| 30 |
)
|
| 31 |
-
from picarones.
|
| 32 |
|
| 33 |
|
| 34 |
def _load_labels(lang: str) -> dict:
|
|
|
|
| 28 |
aggregate_effective_throughput,
|
| 29 |
compute_effective_throughput,
|
| 30 |
)
|
| 31 |
+
from picarones.reports_v2.html.renderers.throughput import build_throughput_html
|
| 32 |
|
| 33 |
|
| 34 |
def _load_labels(lang: str) -> dict:
|
|
@@ -33,7 +33,7 @@ from picarones.measurements.longitudinal import (
|
|
| 33 |
from picarones.measurements.narrative import build_synthesis
|
| 34 |
from picarones.measurements.narrative.detectors import detect_regression_in_history
|
| 35 |
from picarones.core.facts import FactImportance, FactType
|
| 36 |
-
from picarones.
|
| 37 |
|
| 38 |
|
| 39 |
def _load_labels(lang: str) -> dict:
|
|
|
|
| 33 |
from picarones.measurements.narrative import build_synthesis
|
| 34 |
from picarones.measurements.narrative.detectors import detect_regression_in_history
|
| 35 |
from picarones.core.facts import FactImportance, FactType
|
| 36 |
+
from picarones.reports_v2.html.renderers.longitudinal import build_longitudinal_html
|
| 37 |
|
| 38 |
|
| 39 |
def _load_labels(lang: str) -> dict:
|