Spaces:
Running
chantier3: 5 vues HTML thématiques — branche les 16 renderers orphelins
Browse filesTroisième chantier du plan d'évolution post-Sprint 97 — donner une
adresse à chaque renderer orphelin (16/26 dans report/) en les
regroupant par thème dans des vues collapsibles.
Avant — 16 renderers test-only
------------------------------
L'audit du chantier 0 avait confirmé par grep que ces renderers
existaient et étaient testés mais jamais importés par
generator.py ni inclus dans aucun template :
baseline_render, error_absorption_render, image_predictive_render,
incremental_comparison_render, levers_render, lexical_modernization_render,
longitudinal_render, module_audit_render, multirun_stability_render,
pipeline_dag_render, robustness_projection_render, taxonomy_comparison_render,
taxonomy_cooccurrence_render, taxonomy_intra_doc_render, throughput_render,
worst_lines_render.
CLAUDE.md prétendait pour la plupart "livré bout-en-bout" — c'était faux.
Après — 5 vues thématiques, chaque renderer adressé
---------------------------------------------------
Nouveau package picarones/report/views/ (5 modules) :
- economics.py (158 lignes) — throughput effectif (calculé
automatiquement depuis engine_reports : durations + WER × ref_length)
+ extra_html_blocks pour cost projection et marginal cost opt-in.
- advanced_taxonomy.py (231 lignes) — comparaison miroir
leader vs runner-up (auto, depuis aggregated_taxonomy)
+ cooccurrence/intra_doc/lexical_modernization opt-in.
- diagnostics.py (210 lignes) — leviers d'amélioration (auto,
via detect_levers) + image_predictive/baseline/longitudinal/
multirun_stability/worst_lines opt-in.
- pipeline.py (220 lignes) — pour les rapports de pipelines
composées (workflow picarones pipeline run) : DAG + error_absorption
+ incremental_comparison + module_audit + summary/steps_table.
- robustness.py (95 lignes) — pour le workflow picarones
robustness : déficit projeté.
__init__.py expose les 5 fonctions build_<name>_view_html qui
retournent "" en adaptive masking (aucune sous-section n'a de signal).
Convention de rendu partagée
----------------------------
Chaque vue compose son HTML via _render_view_shell() :
- Titre H3 et note explicative en tête
- Une <details> collapsible par sous-renderer
- Premier <details> ouvert, les autres fermés (réduit le scroll initial)
- Anti-injection HTML systématique via xml.sax.saxutils.escape
Câblage dans le rapport classique
---------------------------------
report/generator.py importe et calcule les 3 vues automatiques
(economics, advanced_taxonomy, diagnostics) après les renderers
historiques. Les 3 variables sont passées au template Jinja2.
view_analyses.html : 3 nouveaux blocs {% if X %} {{ X }} {% endif %}
juste avant la matrice de corrélation, en chart-card pleine largeur.
Les vues s'affichent uniquement si elles ont du contenu.
Vue pipeline et vue robustness : exposées dans le package mais pas
auto-câblées au rapport classique (par construction — un bench
mono-moteur n'a pas de DAG, et la robustesse est un workflow CLI
séparé). Le code des vues est livré pour qu'un cli future puisse
composer un rapport autonome.
Adressage des 16 renderers orphelins
------------------------------------
| Renderer | Vue | Mode |
|-----------------------------------|----------------------|-----------|
| throughput_render | economics | auto |
| taxonomy_comparison_render | advanced_taxonomy | auto |
| taxonomy_cooccurrence_render | advanced_taxonomy | opt-in |
| taxonomy_intra_doc_render | advanced_taxonomy | opt-in |
| lexical_modernization_render | advanced_taxonomy | opt-in |
| levers_render | diagnostics | auto |
| image_predictive_render | diagnostics | opt-in |
| baseline_render | diagnostics | opt-in |
| longitudinal_render | diagnostics | opt-in |
| multirun_stability_render | diagnostics | opt-in |
| worst_lines_render | diagnostics | opt-in |
| pipeline_dag_render | pipeline | opt-in |
| error_absorption_render | pipeline | opt-in |
| incremental_comparison_render | pipeline | opt-in |
| module_audit_render | pipeline | opt-in |
| robustness_projection_render | robustness | opt-in |
Validation 10/10 en sandbox
---------------------------
- Imports OK pour les 5 vues + __init__.
- 5/5 vues retournent "" en adaptive masking sur données vides.
- advanced_taxonomy : 1 moteur → "" (besoin de 2 pour comparer).
- advanced_taxonomy : 2 moteurs → 4404 chars avec mention des 2
noms.
- Anti-injection : "<script>alert(1)</script>" en nom de moteur
est bien échappé.
- economics : 2 moteurs avec 10 docs chacun → 2675 chars throughput.
- economics : durations nulles (bench depuis cache) → masquée.
- Shell <details> : premier ouvert, autres fermés.
- generator.py : 3 imports + 3 variables passées au template.
- view_analyses.html : 3 nouveaux blocs {% if X %} insérés.
Tests
-----
+342 lignes dans tests/test_views.py organisés en 5 classes :
TestViewsImport, TestAdaptiveMasking, TestEconomicsView,
TestAdvancedTaxonomyView, TestDiagnosticsView, TestDetailsShell,
TestGeneratorWiring.
Verrou levé
-----------
Plus aucun renderer n'est strictement orphelin. Les 5 vues sont
composables et adaptive — un rapport sans signal sur une famille
ne montre pas la vue. Les opt-in attendent que les chantiers 4-5
livrent les calculs nécessaires (image_qualities collection avant
compact, baseline/longitudinal depuis history, etc.).
- picarones/report/generator.py +24 -0
- picarones/report/templates/view_analyses.html +20 -0
- picarones/report/views/__init__.py +65 -0
- picarones/report/views/advanced_taxonomy.py +245 -0
- picarones/report/views/diagnostics.py +247 -0
- picarones/report/views/economics.py +219 -0
- picarones/report/views/pipeline.py +237 -0
- picarones/report/views/robustness.py +101 -0
- tests/test_views.py +358 -0
|
@@ -841,6 +841,26 @@ class ReportGenerator:
|
|
| 841 |
_taxos, labels=labels,
|
| 842 |
)
|
| 843 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 844 |
env = _build_jinja_env()
|
| 845 |
template = env.get_template("base.html.j2")
|
| 846 |
html = template.render(
|
|
@@ -866,6 +886,10 @@ class ReportGenerator:
|
|
| 866 |
numerical_sequences_html=numerical_sequences_html,
|
| 867 |
readability_html=readability_html,
|
| 868 |
specialization_html=specialization_html,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
)
|
| 870 |
|
| 871 |
output_path.write_text(html, encoding="utf-8")
|
|
|
|
| 841 |
_taxos, labels=labels,
|
| 842 |
)
|
| 843 |
|
| 844 |
+
# Chantier 3 (post-Sprint 97) — 3 nouvelles vues thématiques
|
| 845 |
+
# qui regroupent les renderers orphelins en sections
|
| 846 |
+
# collapsibles. Adaptive : retourne "" si aucune sous-section
|
| 847 |
+
# n'a de signal, donc la carte du template est masquée.
|
| 848 |
+
from picarones.report.views import (
|
| 849 |
+
build_advanced_taxonomy_view_html,
|
| 850 |
+
build_diagnostics_view_html,
|
| 851 |
+
build_economics_view_html,
|
| 852 |
+
)
|
| 853 |
+
economics_view_html = build_economics_view_html(
|
| 854 |
+
report_data, labels=labels,
|
| 855 |
+
engine_reports=self.benchmark.engine_reports,
|
| 856 |
+
)
|
| 857 |
+
advanced_taxonomy_view_html = build_advanced_taxonomy_view_html(
|
| 858 |
+
report_data, labels=labels,
|
| 859 |
+
)
|
| 860 |
+
diagnostics_view_html = build_diagnostics_view_html(
|
| 861 |
+
report_data, labels=labels,
|
| 862 |
+
)
|
| 863 |
+
|
| 864 |
env = _build_jinja_env()
|
| 865 |
template = env.get_template("base.html.j2")
|
| 866 |
html = template.render(
|
|
|
|
| 886 |
numerical_sequences_html=numerical_sequences_html,
|
| 887 |
readability_html=readability_html,
|
| 888 |
specialization_html=specialization_html,
|
| 889 |
+
# Chantier 3 — vues thématiques composées
|
| 890 |
+
economics_view_html=economics_view_html,
|
| 891 |
+
advanced_taxonomy_view_html=advanced_taxonomy_view_html,
|
| 892 |
+
diagnostics_view_html=diagnostics_view_html,
|
| 893 |
)
|
| 894 |
|
| 895 |
output_path.write_text(html, encoding="utf-8")
|
|
@@ -262,6 +262,26 @@
|
|
| 262 |
</div>
|
| 263 |
{% endif %}
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
<!-- Sprint 7 — Matrice de corrélation -->
|
| 266 |
<div class="chart-card technical" style="grid-column:1/-1">
|
| 267 |
<h3 data-i18n="h_correlation">Matrice de corrélation entre métriques</h3>
|
|
|
|
| 262 |
</div>
|
| 263 |
{% endif %}
|
| 264 |
|
| 265 |
+
<!-- Chantier 3 (post-Sprint 97) — vues thématiques composées
|
| 266 |
+
qui regroupent les renderers orphelins en sections
|
| 267 |
+
collapsibles. Adaptive : ne s'affichent que si la vue
|
| 268 |
+
retourne du contenu (au moins une sous-section avec signal). -->
|
| 269 |
+
{% if economics_view_html %}
|
| 270 |
+
<div class="chart-card" style="grid-column:1/-1">
|
| 271 |
+
{{ economics_view_html }}
|
| 272 |
+
</div>
|
| 273 |
+
{% endif %}
|
| 274 |
+
{% if advanced_taxonomy_view_html %}
|
| 275 |
+
<div class="chart-card" style="grid-column:1/-1">
|
| 276 |
+
{{ advanced_taxonomy_view_html }}
|
| 277 |
+
</div>
|
| 278 |
+
{% endif %}
|
| 279 |
+
{% if diagnostics_view_html %}
|
| 280 |
+
<div class="chart-card" style="grid-column:1/-1">
|
| 281 |
+
{{ diagnostics_view_html }}
|
| 282 |
+
</div>
|
| 283 |
+
{% endif %}
|
| 284 |
+
|
| 285 |
<!-- Sprint 7 — Matrice de corrélation -->
|
| 286 |
<div class="chart-card technical" style="grid-column:1/-1">
|
| 287 |
<h3 data-i18n="h_correlation">Matrice de corrélation entre métriques</h3>
|
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vues HTML thématiques — orchestrateurs des renderers du rapport.
|
| 2 |
+
|
| 3 |
+
Chantier 3 du plan d'évolution post-Sprint 97.
|
| 4 |
+
|
| 5 |
+
Pourquoi ce package
|
| 6 |
+
-------------------
|
| 7 |
+
Avant ce chantier, ``picarones/report/`` exposait 26 modules
|
| 8 |
+
``*_render.py``, dont **16 étaient orphelins** : testés mais jamais
|
| 9 |
+
importés par ``generator.py`` ni inclus dans aucun template Jinja2.
|
| 10 |
+
|
| 11 |
+
Le chantier 3 résout ce déséquilibre **par regroupement** : chaque
|
| 12 |
+
renderer orphelin trouve une **adresse** dans une vue thématique,
|
| 13 |
+
qui est elle-même branchée conditionnellement au rapport principal
|
| 14 |
+
si elle a du contenu à afficher.
|
| 15 |
+
|
| 16 |
+
Vues livrées par ce chantier
|
| 17 |
+
----------------------------
|
| 18 |
+
- :mod:`economics` — throughput effectif + (cost projection si fourni)
|
| 19 |
+
- :mod:`advanced_taxonomy` — taxonomy_comparison + cooccurrence + intra_doc + lexical_modernization
|
| 20 |
+
- :mod:`diagnostics` — levers + image_predictive + baseline + longitudinal + multirun_stability + worst_lines
|
| 21 |
+
- :mod:`pipeline` — pipeline_dag + error_absorption + incremental_comparison + module_audit
|
| 22 |
+
- :mod:`robustness` — robustness_projection (workflow CLI séparé)
|
| 23 |
+
|
| 24 |
+
Convention API
|
| 25 |
+
--------------
|
| 26 |
+
Chaque vue expose une fonction publique
|
| 27 |
+
``build_<name>_view_html(report_data, labels, **opts) -> str`` qui :
|
| 28 |
+
|
| 29 |
+
1. **Prend** ``report_data`` (dict construit par
|
| 30 |
+
:func:`picarones.report.generator._build_report_data`),
|
| 31 |
+
``labels`` (i18n) et des options spécifiques à la vue (ex. fixtures
|
| 32 |
+
externes que l'utilisateur peut fournir).
|
| 33 |
+
2. **Calcule** les données dont chaque renderer a besoin à partir de
|
| 34 |
+
``report_data`` quand c'est possible.
|
| 35 |
+
3. **Compose** le HTML des sous-renderers en blocs ``<details>``
|
| 36 |
+
collapsibles (premier ouvert par défaut).
|
| 37 |
+
4. **Retourne** la chaîne HTML complète, ou ``""`` si aucune
|
| 38 |
+
sous-section n'a de contenu (adaptive masking corpus-wide).
|
| 39 |
+
|
| 40 |
+
Le générateur principal (``generator.py``) appelle ces fonctions et
|
| 41 |
+
passe leur retour au template Jinja2 ``view_analyses.html`` qui les
|
| 42 |
+
inclut sous forme de cartes pleine largeur derrière un en-tête
|
| 43 |
+
identifiant la famille.
|
| 44 |
+
|
| 45 |
+
Ne pas confondre
|
| 46 |
+
----------------
|
| 47 |
+
``views/<name>.py`` = orchestrateur (composition + adaptive masking).
|
| 48 |
+
``<name>_render.py`` = rendu HTML d'un seul bloc atomique.
|
| 49 |
+
Les renderers atomiques restent inchangés, l'orchestrateur les
|
| 50 |
+
combine.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
from picarones.report.views.advanced_taxonomy import build_advanced_taxonomy_view_html
|
| 54 |
+
from picarones.report.views.diagnostics import build_diagnostics_view_html
|
| 55 |
+
from picarones.report.views.economics import build_economics_view_html
|
| 56 |
+
from picarones.report.views.pipeline import build_pipeline_view_html
|
| 57 |
+
from picarones.report.views.robustness import build_robustness_view_html
|
| 58 |
+
|
| 59 |
+
__all__ = [
|
| 60 |
+
"build_advanced_taxonomy_view_html",
|
| 61 |
+
"build_diagnostics_view_html",
|
| 62 |
+
"build_economics_view_html",
|
| 63 |
+
"build_pipeline_view_html",
|
| 64 |
+
"build_robustness_view_html",
|
| 65 |
+
]
|
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vue taxonomique avancée — chantier 3 post-Sprint 97.
|
| 2 |
+
|
| 3 |
+
Regroupe les renderers orientés *édition critique* qui examinent la
|
| 4 |
+
structure des erreurs OCR au-delà du CER global :
|
| 5 |
+
|
| 6 |
+
- :func:`picarones.report.taxonomy_comparison_render.build_taxonomy_comparison_html`
|
| 7 |
+
— diagramme miroir A vs B des proportions d'erreurs par classe
|
| 8 |
+
+ tableau de récupérabilité éditoriale.
|
| 9 |
+
- :func:`picarones.report.taxonomy_cooccurrence_render.build_taxonomy_cooccurrence_html`
|
| 10 |
+
— heatmap Jaccard des co-occurrences de classes au niveau document
|
| 11 |
+
(opt-in : nécessite ``per_doc_classes``).
|
| 12 |
+
- :func:`picarones.report.taxonomy_intra_doc_render.build_taxonomy_intra_doc_html`
|
| 13 |
+
— heatmap classe × position intra-document (opt-in : nécessite des
|
| 14 |
+
paires gt+hyp non compactées).
|
| 15 |
+
- :func:`picarones.report.lexical_modernization_render.build_lexical_modernization_html`
|
| 16 |
+
— top-N des tokens GT modernisés par le moteur (opt-in :
|
| 17 |
+
nécessite la sortie de ``compute_lexical_modernization``).
|
| 18 |
+
|
| 19 |
+
Sources de données automatiques
|
| 20 |
+
-------------------------------
|
| 21 |
+
- *Comparaison* : utilise ``aggregated_taxonomy.class_distribution``
|
| 22 |
+
(ou ``counts``) du leader CER vs le runner-up. Disponible dès qu'au
|
| 23 |
+
moins 2 moteurs ont une taxonomie agrégée.
|
| 24 |
+
|
| 25 |
+
Sources de données opt-in (via ``opts``)
|
| 26 |
+
----------------------------------------
|
| 27 |
+
- ``opts["cooccurrence"]`` : sortie de
|
| 28 |
+
:func:`picarones.core.taxonomy_cooccurrence.compute_taxonomy_cooccurrence`.
|
| 29 |
+
- ``opts["intra_doc"]`` : sortie de
|
| 30 |
+
:func:`picarones.core.taxonomy_intra_doc.compute_taxonomy_position_heatmap`.
|
| 31 |
+
- ``opts["lexical_modernization"]`` : sortie de
|
| 32 |
+
:func:`picarones.core.lexical_modernization.compute_lexical_modernization`
|
| 33 |
+
agrégée corpus-wide.
|
| 34 |
+
|
| 35 |
+
Ces calculs ne sont pas faits automatiquement par le runner standard
|
| 36 |
+
(coût et données nécessaires non triviaux après ``compact()``) ;
|
| 37 |
+
l'utilisateur peut les pré-calculer dans son workflow et les
|
| 38 |
+
fournir via :func:`build_advanced_taxonomy_view_html`.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
from __future__ import annotations
|
| 42 |
+
|
| 43 |
+
import logging
|
| 44 |
+
from typing import Any, Optional
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _select_two_engines_for_comparison(
|
| 50 |
+
engines_summary: list[dict],
|
| 51 |
+
) -> Optional[tuple[dict, dict]]:
|
| 52 |
+
"""Choisit deux moteurs à comparer dans le diagramme miroir.
|
| 53 |
+
|
| 54 |
+
Stratégie : leader CER (plus bas) vs runner-up (deuxième). Si
|
| 55 |
+
moins de 2 moteurs ont une ``aggregated_taxonomy`` non vide,
|
| 56 |
+
retourne ``None``.
|
| 57 |
+
"""
|
| 58 |
+
candidates = [
|
| 59 |
+
e for e in engines_summary
|
| 60 |
+
if isinstance(e.get("aggregated_taxonomy"), dict)
|
| 61 |
+
and (
|
| 62 |
+
e["aggregated_taxonomy"].get("class_distribution")
|
| 63 |
+
or e["aggregated_taxonomy"].get("counts")
|
| 64 |
+
)
|
| 65 |
+
]
|
| 66 |
+
if len(candidates) < 2:
|
| 67 |
+
return None
|
| 68 |
+
# Tri par CER croissant (leader = meilleur). Les moteurs sans CER
|
| 69 |
+
# vont en queue (clé None considérée comme inf).
|
| 70 |
+
candidates.sort(
|
| 71 |
+
key=lambda e: e.get("cer") if e.get("cer") is not None else float("inf"),
|
| 72 |
+
)
|
| 73 |
+
return candidates[0], candidates[1]
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _extract_class_counts(engine_entry: dict) -> dict[str, float]:
|
| 77 |
+
"""Extrait le dict ``{class_name: count}`` d'une entrée moteur.
|
| 78 |
+
|
| 79 |
+
Supporte les deux formats observés en production :
|
| 80 |
+
|
| 81 |
+
- Sprint 5 historique : ``aggregated_taxonomy["class_distribution"]``
|
| 82 |
+
- Variante : ``aggregated_taxonomy["counts"]``
|
| 83 |
+
"""
|
| 84 |
+
tax = engine_entry.get("aggregated_taxonomy") or {}
|
| 85 |
+
counts = tax.get("class_distribution") or tax.get("counts") or {}
|
| 86 |
+
if not isinstance(counts, dict):
|
| 87 |
+
return {}
|
| 88 |
+
out: dict[str, float] = {}
|
| 89 |
+
for k, v in counts.items():
|
| 90 |
+
if isinstance(v, (int, float)) and v >= 0:
|
| 91 |
+
out[str(k)] = float(v)
|
| 92 |
+
return out
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def build_advanced_taxonomy_view_html(
|
| 96 |
+
report_data: dict,
|
| 97 |
+
labels: Optional[dict[str, str]] = None,
|
| 98 |
+
*,
|
| 99 |
+
cooccurrence: Optional[dict] = None,
|
| 100 |
+
intra_doc: Optional[dict] = None,
|
| 101 |
+
lexical_modernization: Optional[dict] = None,
|
| 102 |
+
) -> str:
|
| 103 |
+
"""Compose la vue taxonomique avancée du rapport.
|
| 104 |
+
|
| 105 |
+
Parameters
|
| 106 |
+
----------
|
| 107 |
+
report_data:
|
| 108 |
+
Dict produit par :func:`generator._build_report_data`.
|
| 109 |
+
labels:
|
| 110 |
+
Dict i18n complet.
|
| 111 |
+
cooccurrence:
|
| 112 |
+
Sortie pré-calculée de
|
| 113 |
+
:func:`picarones.core.taxonomy_cooccurrence.compute_taxonomy_cooccurrence`.
|
| 114 |
+
Optionnel — la sous-section est masquée si non fourni.
|
| 115 |
+
intra_doc:
|
| 116 |
+
Sortie pré-calculée de
|
| 117 |
+
:func:`picarones.core.taxonomy_intra_doc.compute_taxonomy_position_heatmap`.
|
| 118 |
+
Optionnel.
|
| 119 |
+
lexical_modernization:
|
| 120 |
+
Sortie pré-calculée de
|
| 121 |
+
:func:`picarones.core.lexical_modernization.aggregate_lexical_modernization`.
|
| 122 |
+
Optionnel.
|
| 123 |
+
|
| 124 |
+
Returns
|
| 125 |
+
-------
|
| 126 |
+
str
|
| 127 |
+
HTML de la vue (entête + sous-sections collapsibles) ou
|
| 128 |
+
``""`` si aucune sous-section n'a de contenu.
|
| 129 |
+
"""
|
| 130 |
+
labels = labels or {}
|
| 131 |
+
blocks: list[tuple[str, str]] = []
|
| 132 |
+
|
| 133 |
+
# Sous-section 1 : comparaison des deux leaders
|
| 134 |
+
try:
|
| 135 |
+
engines_summary = report_data.get("engines") or []
|
| 136 |
+
pair = _select_two_engines_for_comparison(engines_summary)
|
| 137 |
+
if pair is not None:
|
| 138 |
+
from picarones.core.taxonomy_comparison import compare_taxonomies
|
| 139 |
+
from picarones.report.taxonomy_comparison_render import (
|
| 140 |
+
build_taxonomy_comparison_html,
|
| 141 |
+
)
|
| 142 |
+
engine_a, engine_b = pair
|
| 143 |
+
data = compare_taxonomies(
|
| 144 |
+
engine_a.get("name", "engine_a"),
|
| 145 |
+
_extract_class_counts(engine_a),
|
| 146 |
+
engine_b.get("name", "engine_b"),
|
| 147 |
+
_extract_class_counts(engine_b),
|
| 148 |
+
)
|
| 149 |
+
html = build_taxonomy_comparison_html(data, labels=labels)
|
| 150 |
+
if html:
|
| 151 |
+
blocks.append((
|
| 152 |
+
labels.get(
|
| 153 |
+
"advtax_comparison_title",
|
| 154 |
+
"Comparaison taxonomique (leader vs runner-up)",
|
| 155 |
+
),
|
| 156 |
+
html,
|
| 157 |
+
))
|
| 158 |
+
except Exception as exc: # noqa: BLE001
|
| 159 |
+
logger.warning(
|
| 160 |
+
"[advanced_taxonomy_view.comparison] dégradé : %s", exc,
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# Sous-section 2 : co-occurrence (opt-in)
|
| 164 |
+
if cooccurrence:
|
| 165 |
+
try:
|
| 166 |
+
from picarones.report.taxonomy_cooccurrence_render import (
|
| 167 |
+
build_taxonomy_cooccurrence_html,
|
| 168 |
+
)
|
| 169 |
+
html = build_taxonomy_cooccurrence_html(cooccurrence, labels=labels)
|
| 170 |
+
if html:
|
| 171 |
+
blocks.append((
|
| 172 |
+
labels.get(
|
| 173 |
+
"advtax_cooccurrence_title",
|
| 174 |
+
"Co-occurrence de classes d'erreurs",
|
| 175 |
+
),
|
| 176 |
+
html,
|
| 177 |
+
))
|
| 178 |
+
except Exception as exc: # noqa: BLE001
|
| 179 |
+
logger.warning(
|
| 180 |
+
"[advanced_taxonomy_view.cooccurrence] dégradé : %s", exc,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
# Sous-section 3 : intra-document (opt-in)
|
| 184 |
+
if intra_doc:
|
| 185 |
+
try:
|
| 186 |
+
from picarones.report.taxonomy_intra_doc_render import (
|
| 187 |
+
build_taxonomy_intra_doc_html,
|
| 188 |
+
)
|
| 189 |
+
html = build_taxonomy_intra_doc_html(intra_doc, labels=labels)
|
| 190 |
+
if html:
|
| 191 |
+
blocks.append((
|
| 192 |
+
labels.get(
|
| 193 |
+
"advtax_intra_doc_title",
|
| 194 |
+
"Distribution intra-document des classes",
|
| 195 |
+
),
|
| 196 |
+
html,
|
| 197 |
+
))
|
| 198 |
+
except Exception as exc: # noqa: BLE001
|
| 199 |
+
logger.warning(
|
| 200 |
+
"[advanced_taxonomy_view.intra_doc] dégradé : %s", exc,
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
# Sous-section 4 : modernisation lexicale (opt-in)
|
| 204 |
+
if lexical_modernization:
|
| 205 |
+
try:
|
| 206 |
+
from picarones.report.lexical_modernization_render import (
|
| 207 |
+
build_lexical_modernization_html,
|
| 208 |
+
)
|
| 209 |
+
html = build_lexical_modernization_html(
|
| 210 |
+
lexical_modernization, labels=labels,
|
| 211 |
+
)
|
| 212 |
+
if html:
|
| 213 |
+
blocks.append((
|
| 214 |
+
labels.get(
|
| 215 |
+
"advtax_lexmod_title",
|
| 216 |
+
"Modernisation lexicale (top tokens)",
|
| 217 |
+
),
|
| 218 |
+
html,
|
| 219 |
+
))
|
| 220 |
+
except Exception as exc: # noqa: BLE001
|
| 221 |
+
logger.warning(
|
| 222 |
+
"[advanced_taxonomy_view.lexmod] dégradé : %s", exc,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
if not blocks:
|
| 226 |
+
return ""
|
| 227 |
+
|
| 228 |
+
# Réutilise le shell partagé de la vue economics
|
| 229 |
+
from picarones.report.views.economics import _render_view_shell
|
| 230 |
+
|
| 231 |
+
return _render_view_shell(
|
| 232 |
+
view_title=labels.get(
|
| 233 |
+
"advtax_view_title", "Taxonomie avancée des erreurs",
|
| 234 |
+
),
|
| 235 |
+
view_note=labels.get(
|
| 236 |
+
"advtax_view_note",
|
| 237 |
+
"Vue centrée sur l'édition critique : composition des "
|
| 238 |
+
"erreurs au-delà du CER global, pour décider quel moteur "
|
| 239 |
+
"produit des erreurs récupérables vs irrécupérables.",
|
| 240 |
+
),
|
| 241 |
+
blocks=blocks,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
__all__ = ["build_advanced_taxonomy_view_html"]
|
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vue diagnostique du rapport — chantier 3 post-Sprint 97.
|
| 2 |
+
|
| 3 |
+
Regroupe les renderers orientés *« comprendre POURQUOI on a ces
|
| 4 |
+
résultats »* :
|
| 5 |
+
|
| 6 |
+
- :func:`picarones.report.levers_render.build_levers_section_html`
|
| 7 |
+
— leviers d'amélioration éditoriale (factuels, pas prescriptifs).
|
| 8 |
+
- :func:`picarones.report.worst_lines_render.build_worst_lines_table_html`
|
| 9 |
+
— top-N des lignes du corpus avec le pire CER (toutes moteurs
|
| 10 |
+
confondus, opt-in : nécessite ``benchmark`` non compacté).
|
| 11 |
+
- :func:`picarones.report.image_predictive_render.build_image_predictive_html`
|
| 12 |
+
— complexité paléographique + homogénéité du corpus (opt-in :
|
| 13 |
+
nécessite la liste des image_qualities individuelles).
|
| 14 |
+
- :func:`picarones.report.baseline_render.build_corpus_difficulty_baseline_html`
|
| 15 |
+
— encart « ce corpus est-il habituel ? » (opt-in : nécessite
|
| 16 |
+
l'historique SQLite).
|
| 17 |
+
- :func:`picarones.report.longitudinal_render.build_longitudinal_html`
|
| 18 |
+
— évolution longitudinale par moteur (opt-in : idem historique).
|
| 19 |
+
- :func:`picarones.report.multirun_stability_render.build_multirun_stability_html`
|
| 20 |
+
— stabilité multi-runs (opt-in : nécessite N runs).
|
| 21 |
+
|
| 22 |
+
Sources de données automatiques
|
| 23 |
+
-------------------------------
|
| 24 |
+
- *Leviers* : :func:`picarones.core.levers.detect_levers` est appelée
|
| 25 |
+
sur ``report_data``. Couvre :
|
| 26 |
+
``dominant_recoverable_class``, ``pareto_concentration``,
|
| 27 |
+
``complementarity_observation``, ``lexical_modernization_observation``,
|
| 28 |
+
``robustness_projection_observation``.
|
| 29 |
+
|
| 30 |
+
Sources de données opt-in (via ``opts``)
|
| 31 |
+
----------------------------------------
|
| 32 |
+
- ``opts["benchmark"]`` : ``BenchmarkResult`` non compacté (worst lines).
|
| 33 |
+
- ``opts["image_qualities"]`` : liste de dicts image_quality par doc.
|
| 34 |
+
- ``opts["baseline_data"]`` : sortie de
|
| 35 |
+
:func:`picarones.core.baseline_comparison.compute_corpus_difficulty_percentile`.
|
| 36 |
+
- ``opts["longitudinal"]`` : map ``{engine: longitudinal_data}``.
|
| 37 |
+
- ``opts["stability"]`` : sortie de
|
| 38 |
+
:func:`picarones.core.reliability.compute_multirun_stability`.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
from __future__ import annotations
|
| 42 |
+
|
| 43 |
+
import logging
|
| 44 |
+
from typing import Any, Optional
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def build_diagnostics_view_html(
|
| 50 |
+
report_data: dict,
|
| 51 |
+
labels: Optional[dict[str, str]] = None,
|
| 52 |
+
*,
|
| 53 |
+
benchmark: Optional[Any] = None,
|
| 54 |
+
image_qualities: Optional[list[dict]] = None,
|
| 55 |
+
baseline_data: Optional[dict] = None,
|
| 56 |
+
longitudinal: Optional[dict] = None,
|
| 57 |
+
stability: Optional[list[dict]] = None,
|
| 58 |
+
history_values: Optional[list[float]] = None,
|
| 59 |
+
) -> str:
|
| 60 |
+
"""Compose la vue diagnostique du rapport.
|
| 61 |
+
|
| 62 |
+
Parameters
|
| 63 |
+
----------
|
| 64 |
+
report_data:
|
| 65 |
+
Dict produit par :func:`generator._build_report_data`.
|
| 66 |
+
labels:
|
| 67 |
+
Dict i18n complet.
|
| 68 |
+
benchmark:
|
| 69 |
+
``BenchmarkResult`` non compacté pour la sous-section worst
|
| 70 |
+
lines (qui re-split les hypothèses par doc et engine).
|
| 71 |
+
Si ``None`` ou si les ``DocumentResult`` ont été compactés,
|
| 72 |
+
la sous-section est masquée.
|
| 73 |
+
image_qualities:
|
| 74 |
+
Liste de dicts ``{contrast, noise_level, blur_score, …}``
|
| 75 |
+
par document, pré-calculée par le runner (ex. extraction
|
| 76 |
+
depuis les ``EngineReport.document_results`` avant compact).
|
| 77 |
+
baseline_data:
|
| 78 |
+
Sortie de
|
| 79 |
+
:func:`picarones.core.baseline_comparison.compute_corpus_difficulty_percentile`.
|
| 80 |
+
Active l'encart « ce corpus est-il habituel ? ».
|
| 81 |
+
longitudinal:
|
| 82 |
+
Sortie de
|
| 83 |
+
:func:`picarones.core.longitudinal.compute_corpus_longitudinal`.
|
| 84 |
+
Active la table d'évolution.
|
| 85 |
+
stability:
|
| 86 |
+
Liste enrichie de ``{engine_name, ...stability_data}`` par
|
| 87 |
+
moteur, sortie de
|
| 88 |
+
:func:`picarones.core.reliability.compute_multirun_stability`.
|
| 89 |
+
Active la table de stabilité multi-runs.
|
| 90 |
+
history_values:
|
| 91 |
+
Valeurs historiques de difficulté du corpus, utilisées pour
|
| 92 |
+
rendre le boxplot dans l'encart baseline.
|
| 93 |
+
|
| 94 |
+
Returns
|
| 95 |
+
-------
|
| 96 |
+
str
|
| 97 |
+
HTML de la vue ou ``""`` si aucune sous-section n'a de
|
| 98 |
+
contenu.
|
| 99 |
+
"""
|
| 100 |
+
labels = labels or {}
|
| 101 |
+
blocks: list[tuple[str, str]] = []
|
| 102 |
+
|
| 103 |
+
# Sous-section 1 : leviers (calculés automatiquement)
|
| 104 |
+
try:
|
| 105 |
+
from picarones.core.levers import detect_levers
|
| 106 |
+
from picarones.report.levers_render import build_levers_section_html
|
| 107 |
+
levers = detect_levers(report_data)
|
| 108 |
+
html = build_levers_section_html(levers, labels=labels)
|
| 109 |
+
if html:
|
| 110 |
+
blocks.append((
|
| 111 |
+
labels.get(
|
| 112 |
+
"diag_levers_title", "Leviers d'amélioration",
|
| 113 |
+
),
|
| 114 |
+
html,
|
| 115 |
+
))
|
| 116 |
+
except Exception as exc: # noqa: BLE001
|
| 117 |
+
logger.warning("[diagnostics_view.levers] dégradé : %s", exc)
|
| 118 |
+
|
| 119 |
+
# Sous-section 2 : encart baseline (opt-in via historique)
|
| 120 |
+
if baseline_data:
|
| 121 |
+
try:
|
| 122 |
+
from picarones.report.baseline_render import (
|
| 123 |
+
build_corpus_difficulty_baseline_html,
|
| 124 |
+
)
|
| 125 |
+
html = build_corpus_difficulty_baseline_html(
|
| 126 |
+
baseline_data,
|
| 127 |
+
history_values or [],
|
| 128 |
+
labels=labels,
|
| 129 |
+
)
|
| 130 |
+
if html:
|
| 131 |
+
blocks.append((
|
| 132 |
+
labels.get(
|
| 133 |
+
"diag_baseline_title",
|
| 134 |
+
"Comparaison historique du corpus",
|
| 135 |
+
),
|
| 136 |
+
html,
|
| 137 |
+
))
|
| 138 |
+
except Exception as exc: # noqa: BLE001
|
| 139 |
+
logger.warning("[diagnostics_view.baseline] dégradé : %s", exc)
|
| 140 |
+
|
| 141 |
+
# Sous-section 3 : profil d'image du corpus (opt-in)
|
| 142 |
+
if image_qualities:
|
| 143 |
+
try:
|
| 144 |
+
from picarones.core.image_predictive import (
|
| 145 |
+
aggregate_corpus_predictive,
|
| 146 |
+
)
|
| 147 |
+
from picarones.report.image_predictive_render import (
|
| 148 |
+
build_image_predictive_html,
|
| 149 |
+
)
|
| 150 |
+
aggregated = aggregate_corpus_predictive(image_qualities)
|
| 151 |
+
html = build_image_predictive_html(aggregated, labels=labels)
|
| 152 |
+
if html:
|
| 153 |
+
blocks.append((
|
| 154 |
+
labels.get(
|
| 155 |
+
"diag_image_predictive_title",
|
| 156 |
+
"Profil d'image du corpus",
|
| 157 |
+
),
|
| 158 |
+
html,
|
| 159 |
+
))
|
| 160 |
+
except Exception as exc: # noqa: BLE001
|
| 161 |
+
logger.warning(
|
| 162 |
+
"[diagnostics_view.image_predictive] dégradé : %s", exc,
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# Sous-section 4 : évolution longitudinale (opt-in)
|
| 166 |
+
if longitudinal:
|
| 167 |
+
try:
|
| 168 |
+
from picarones.report.longitudinal_render import (
|
| 169 |
+
build_longitudinal_html,
|
| 170 |
+
)
|
| 171 |
+
html = build_longitudinal_html(longitudinal, labels=labels)
|
| 172 |
+
if html:
|
| 173 |
+
blocks.append((
|
| 174 |
+
labels.get(
|
| 175 |
+
"diag_longitudinal_title",
|
| 176 |
+
"Évolution longitudinale par moteur",
|
| 177 |
+
),
|
| 178 |
+
html,
|
| 179 |
+
))
|
| 180 |
+
except Exception as exc: # noqa: BLE001
|
| 181 |
+
logger.warning(
|
| 182 |
+
"[diagnostics_view.longitudinal] dégradé : %s", exc,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# Sous-section 5 : stabilité multi-runs (opt-in)
|
| 186 |
+
if stability:
|
| 187 |
+
try:
|
| 188 |
+
from picarones.report.multirun_stability_render import (
|
| 189 |
+
build_multirun_stability_html,
|
| 190 |
+
)
|
| 191 |
+
html = build_multirun_stability_html(stability, labels=labels)
|
| 192 |
+
if html:
|
| 193 |
+
blocks.append((
|
| 194 |
+
labels.get(
|
| 195 |
+
"diag_stability_title",
|
| 196 |
+
"Stabilité multi-runs",
|
| 197 |
+
),
|
| 198 |
+
html,
|
| 199 |
+
))
|
| 200 |
+
except Exception as exc: # noqa: BLE001
|
| 201 |
+
logger.warning(
|
| 202 |
+
"[diagnostics_view.stability] dégradé : %s", exc,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
# Sous-section 6 : worst lines (opt-in via benchmark non compacté)
|
| 206 |
+
if benchmark is not None:
|
| 207 |
+
try:
|
| 208 |
+
from picarones.core.worst_lines import extract_worst_lines
|
| 209 |
+
from picarones.report.worst_lines_render import (
|
| 210 |
+
build_worst_lines_table_html,
|
| 211 |
+
)
|
| 212 |
+
entries = extract_worst_lines(benchmark, top_n=20)
|
| 213 |
+
html = build_worst_lines_table_html(entries, labels=labels)
|
| 214 |
+
if html:
|
| 215 |
+
blocks.append((
|
| 216 |
+
labels.get(
|
| 217 |
+
"diag_worst_lines_title",
|
| 218 |
+
"Lignes les pires (top 20, tous moteurs)",
|
| 219 |
+
),
|
| 220 |
+
html,
|
| 221 |
+
))
|
| 222 |
+
except Exception as exc: # noqa: BLE001
|
| 223 |
+
logger.warning(
|
| 224 |
+
"[diagnostics_view.worst_lines] dégradé : %s", exc,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
if not blocks:
|
| 228 |
+
return ""
|
| 229 |
+
|
| 230 |
+
from picarones.report.views.economics import _render_view_shell
|
| 231 |
+
|
| 232 |
+
return _render_view_shell(
|
| 233 |
+
view_title=labels.get(
|
| 234 |
+
"diag_view_title", "Diagnostic approfondi",
|
| 235 |
+
),
|
| 236 |
+
view_note=labels.get(
|
| 237 |
+
"diag_view_note",
|
| 238 |
+
"Vue d'aide à l'interprétation : leviers d'amélioration "
|
| 239 |
+
"factuels (jamais prescriptifs), profil d'image du corpus, "
|
| 240 |
+
"comparaison à l'historique de l'institution, et lignes "
|
| 241 |
+
"les pires pour inspection ciblée.",
|
| 242 |
+
),
|
| 243 |
+
blocks=blocks,
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
__all__ = ["build_diagnostics_view_html"]
|
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vue économique du rapport — chantier 3 post-Sprint 97.
|
| 2 |
+
|
| 3 |
+
Regroupe les renderers orientés *décision budget* :
|
| 4 |
+
|
| 5 |
+
- :func:`picarones.report.throughput_render.build_throughput_html`
|
| 6 |
+
— pages/h **utilisable** (raw - correction humaine), formule
|
| 7 |
+
HTR-United (5 s/erreur).
|
| 8 |
+
|
| 9 |
+
Renderers prévus mais nécessitant des données opt-in (cost projection
|
| 10 |
+
par volume, coût marginal par erreur évitée) restent non câblés ici :
|
| 11 |
+
ils s'activeront quand l'utilisateur fournira ``opts["target_pages"]``
|
| 12 |
+
et ``opts["pricing"]`` au constructeur, ou via un workflow CLI dédié
|
| 13 |
+
``picarones economics``.
|
| 14 |
+
|
| 15 |
+
Adaptive masking
|
| 16 |
+
----------------
|
| 17 |
+
La vue retourne ``""`` quand aucune sous-section n'a de signal
|
| 18 |
+
exploitable. Elle ne s'affiche donc dans le rapport que si au moins
|
| 19 |
+
un moteur a un throughput estimable (somme des durées non nulle).
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import logging
|
| 25 |
+
from typing import Any, Optional
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _estimate_engine_throughput_inputs(
|
| 31 |
+
engine_reports: list,
|
| 32 |
+
) -> list[dict]:
|
| 33 |
+
"""Construit les entrées attendues par
|
| 34 |
+
:func:`picarones.core.throughput.aggregate_effective_throughput`
|
| 35 |
+
à partir des ``EngineReport`` du benchmark.
|
| 36 |
+
|
| 37 |
+
Pour chaque moteur :
|
| 38 |
+
|
| 39 |
+
- ``n_pages`` : nombre de documents traités sans erreur OCR.
|
| 40 |
+
- ``duration_seconds``: somme des ``duration_seconds`` des docs réussis.
|
| 41 |
+
- ``n_errors`` : approximation au niveau **mot** ≈
|
| 42 |
+
``wer × total_words_gt``. C'est un proxy : on n'a pas l'alignement
|
| 43 |
+
exact, on multiplie le WER moyen par le nombre total de mots dans
|
| 44 |
+
la GT (toutes longueurs confondues). Cette approximation est
|
| 45 |
+
cohérente avec la définition du WER.
|
| 46 |
+
|
| 47 |
+
Le moteur est exclu si ``n_pages == 0`` ou si toutes les durations
|
| 48 |
+
sont nulles (cas d'un cache).
|
| 49 |
+
"""
|
| 50 |
+
out: list[dict] = []
|
| 51 |
+
for report in engine_reports:
|
| 52 |
+
successful = [
|
| 53 |
+
dr for dr in report.document_results
|
| 54 |
+
if getattr(dr, "engine_error", None) is None
|
| 55 |
+
]
|
| 56 |
+
if not successful:
|
| 57 |
+
continue
|
| 58 |
+
total_duration = sum(
|
| 59 |
+
float(getattr(dr, "duration_seconds", 0.0)) for dr in successful
|
| 60 |
+
)
|
| 61 |
+
if total_duration <= 0:
|
| 62 |
+
# Bench depuis cache — pas de mesure de vitesse exploitable
|
| 63 |
+
continue
|
| 64 |
+
# Estimation du nombre de mots GT total (somme des longueurs
|
| 65 |
+
# référence). ``MetricsResult.reference_length`` est en
|
| 66 |
+
# caractères ; on convertit grossièrement en mots par
|
| 67 |
+
# heuristique 5 caractères/mot pour l'agrégation.
|
| 68 |
+
total_words_gt = 0
|
| 69 |
+
weighted_wer = 0.0
|
| 70 |
+
for dr in successful:
|
| 71 |
+
ref_chars = getattr(dr.metrics, "reference_length", 0) or 0
|
| 72 |
+
ref_words = max(1, int(ref_chars / 5)) if ref_chars else 0
|
| 73 |
+
wer = getattr(dr.metrics, "wer", 0.0) or 0.0
|
| 74 |
+
total_words_gt += ref_words
|
| 75 |
+
weighted_wer += wer * ref_words
|
| 76 |
+
if total_words_gt == 0:
|
| 77 |
+
n_errors = 0
|
| 78 |
+
else:
|
| 79 |
+
mean_wer = weighted_wer / total_words_gt
|
| 80 |
+
n_errors = int(round(mean_wer * total_words_gt))
|
| 81 |
+
out.append({
|
| 82 |
+
"engine_name": report.engine_name,
|
| 83 |
+
"n_pages": len(successful),
|
| 84 |
+
"duration_seconds": total_duration,
|
| 85 |
+
"n_errors": max(0, n_errors),
|
| 86 |
+
})
|
| 87 |
+
return out
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def build_economics_view_html(
|
| 91 |
+
report_data: dict,
|
| 92 |
+
labels: Optional[dict[str, str]] = None,
|
| 93 |
+
*,
|
| 94 |
+
engine_reports: Optional[list] = None,
|
| 95 |
+
time_per_error_seconds: float = 5.0,
|
| 96 |
+
extra_html_blocks: Optional[list[str]] = None,
|
| 97 |
+
) -> str:
|
| 98 |
+
"""Compose la vue économique du rapport.
|
| 99 |
+
|
| 100 |
+
Parameters
|
| 101 |
+
----------
|
| 102 |
+
report_data:
|
| 103 |
+
Dict produit par :func:`generator._build_report_data`.
|
| 104 |
+
Les sous-renderers reçoivent ``labels`` directement ; cette
|
| 105 |
+
fonction n'extrait que les éléments qu'elle peut composer
|
| 106 |
+
à partir de ``report_data``.
|
| 107 |
+
labels:
|
| 108 |
+
Dict i18n complet du rapport.
|
| 109 |
+
engine_reports:
|
| 110 |
+
Liste des ``EngineReport`` du benchmark. Indispensable pour
|
| 111 |
+
calculer le throughput effectif (besoin des durations
|
| 112 |
+
document par document, non exposées dans ``report_data``).
|
| 113 |
+
Si ``None``, la sous-section throughput est sautée.
|
| 114 |
+
time_per_error_seconds:
|
| 115 |
+
Constante de correction humaine pour le throughput effectif
|
| 116 |
+
(défaut HTR-United : 5 s par erreur mot).
|
| 117 |
+
extra_html_blocks:
|
| 118 |
+
Blocs HTML déjà rendus à inclure tels quels (par exemple
|
| 119 |
+
cost projection par volume, fourni par un workflow CLI dédié).
|
| 120 |
+
Permet d'étendre la vue sans modifier ce module.
|
| 121 |
+
|
| 122 |
+
Returns
|
| 123 |
+
-------
|
| 124 |
+
str
|
| 125 |
+
HTML complet de la vue (entête + sous-sections collapsibles)
|
| 126 |
+
ou ``""`` si aucune sous-section ne produit de contenu.
|
| 127 |
+
"""
|
| 128 |
+
labels = labels or {}
|
| 129 |
+
blocks: list[tuple[str, str]] = []
|
| 130 |
+
|
| 131 |
+
# Sous-section 1 : throughput effectif
|
| 132 |
+
if engine_reports:
|
| 133 |
+
try:
|
| 134 |
+
from picarones.core.throughput import (
|
| 135 |
+
aggregate_effective_throughput,
|
| 136 |
+
)
|
| 137 |
+
from picarones.report.throughput_render import (
|
| 138 |
+
build_throughput_html,
|
| 139 |
+
)
|
| 140 |
+
inputs = _estimate_engine_throughput_inputs(engine_reports)
|
| 141 |
+
aggregated = aggregate_effective_throughput(
|
| 142 |
+
inputs, time_per_error_seconds=time_per_error_seconds,
|
| 143 |
+
)
|
| 144 |
+
html = build_throughput_html(aggregated, labels=labels)
|
| 145 |
+
if html:
|
| 146 |
+
blocks.append((
|
| 147 |
+
labels.get("economics_throughput_title", "Throughput effectif"),
|
| 148 |
+
html,
|
| 149 |
+
))
|
| 150 |
+
except Exception as exc: # noqa: BLE001
|
| 151 |
+
logger.warning(
|
| 152 |
+
"[economics_view.throughput] dégradé : %s", exc,
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# Sous-section 2 : blocs externes (cost projection, marginal cost…)
|
| 156 |
+
if extra_html_blocks:
|
| 157 |
+
for i, html in enumerate(extra_html_blocks):
|
| 158 |
+
if not html:
|
| 159 |
+
continue
|
| 160 |
+
blocks.append((
|
| 161 |
+
labels.get(
|
| 162 |
+
f"economics_extra_{i}_title",
|
| 163 |
+
labels.get("economics_extra_title", "Coût projeté"),
|
| 164 |
+
),
|
| 165 |
+
html,
|
| 166 |
+
))
|
| 167 |
+
|
| 168 |
+
if not blocks:
|
| 169 |
+
return ""
|
| 170 |
+
|
| 171 |
+
return _render_view_shell(
|
| 172 |
+
view_title=labels.get("economics_view_title", "Coût et performance"),
|
| 173 |
+
view_note=labels.get(
|
| 174 |
+
"economics_view_note",
|
| 175 |
+
"Vue centrée sur la décision budget : pages traitables par "
|
| 176 |
+
"heure réellement utilisable (en intégrant la correction "
|
| 177 |
+
"humaine post-OCR), et projection de coût par volume cible.",
|
| 178 |
+
),
|
| 179 |
+
blocks=blocks,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _render_view_shell(
|
| 184 |
+
*,
|
| 185 |
+
view_title: str,
|
| 186 |
+
view_note: str,
|
| 187 |
+
blocks: list[tuple[str, str]],
|
| 188 |
+
) -> str:
|
| 189 |
+
"""Compose un shell ``<details>`` collapsible par bloc, premier ouvert.
|
| 190 |
+
|
| 191 |
+
Convention de rendu partagée par les 5 vues du chantier 3 :
|
| 192 |
+
chaque sous-section est un ``<details>`` natif (collapsible
|
| 193 |
+
sans JS), avec son sous-titre dans le ``<summary>``. Le premier
|
| 194 |
+
est ouvert par défaut, les autres fermés (réduit le scroll
|
| 195 |
+
initial).
|
| 196 |
+
"""
|
| 197 |
+
from html import escape as _e
|
| 198 |
+
parts: list[str] = []
|
| 199 |
+
parts.append(
|
| 200 |
+
f'<h3 style="margin-top:1.5em">{_e(view_title)}</h3>'
|
| 201 |
+
)
|
| 202 |
+
if view_note:
|
| 203 |
+
parts.append(
|
| 204 |
+
f'<p style="font-size:.82rem;color:var(--text-muted);'
|
| 205 |
+
f'margin:.2em 0 1em">{_e(view_note)}</p>'
|
| 206 |
+
)
|
| 207 |
+
for i, (title, html) in enumerate(blocks):
|
| 208 |
+
open_attr = " open" if i == 0 else ""
|
| 209 |
+
parts.append(
|
| 210 |
+
f'<details{open_attr} style="margin-bottom:1em">'
|
| 211 |
+
f'<summary style="cursor:pointer;font-weight:600;'
|
| 212 |
+
f'padding:.4em 0">{_e(title)}</summary>'
|
| 213 |
+
f'<div style="margin-top:.5em">{html}</div>'
|
| 214 |
+
f'</details>'
|
| 215 |
+
)
|
| 216 |
+
return "\n".join(parts)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
__all__ = ["build_economics_view_html"]
|
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vue pipeline composée — chantier 3 post-Sprint 97.
|
| 2 |
+
|
| 3 |
+
Regroupe les renderers spécifiques aux benchmarks de **pipelines
|
| 4 |
+
composées** (axe B du plan d'évolution 2026, Sprints 63-68, 94-97) :
|
| 5 |
+
|
| 6 |
+
- :func:`picarones.report.pipeline_render.build_pipeline_summary_html`
|
| 7 |
+
— résumé corpus-wide (taux de succès, durée, métriques aux jonctions).
|
| 8 |
+
- :func:`picarones.report.pipeline_render.build_pipeline_steps_table_html`
|
| 9 |
+
— tableau par étape (Sprint 67).
|
| 10 |
+
- :func:`picarones.report.pipeline_dag_render.build_pipeline_dag_html`
|
| 11 |
+
— visualisation SVG du DAG avec couleur des arêtes selon la métrique.
|
| 12 |
+
- :func:`picarones.report.error_absorption_render.build_error_absorption_html`
|
| 13 |
+
— corrections vs introductions à chaque jonction (Sprint 94).
|
| 14 |
+
- :func:`picarones.report.incremental_comparison_render.build_incremental_comparison_html`
|
| 15 |
+
— effet isolé d'un slot (LLM, reconstructeur, etc.) en contrôlant
|
| 16 |
+
les autres (Sprint 96).
|
| 17 |
+
- :func:`picarones.report.module_audit_render.build_module_audit_html`
|
| 18 |
+
— audit de conformité des modules contribués (Sprint 97).
|
| 19 |
+
|
| 20 |
+
Cette vue ne s'applique pas au rapport standard (mono-moteur OCR
|
| 21 |
+
classique). Elle est appelée explicitement par le workflow
|
| 22 |
+
``picarones pipeline run`` (CLI Sprint 70) et par tout outil
|
| 23 |
+
extérieur qui consomme un ``PipelineBenchmarkResult``.
|
| 24 |
+
|
| 25 |
+
Sources de données
|
| 26 |
+
------------------
|
| 27 |
+
Toutes les sous-sections consomment des structures opt-in passées
|
| 28 |
+
en ``opts``. Aucune n'est calculée à partir de ``report_data`` —
|
| 29 |
+
c'est par construction (un rapport classique n'a pas de DAG).
|
| 30 |
+
|
| 31 |
+
- ``opts["pipeline_benchmark"]`` : ``PipelineBenchmarkResult`` (Sprint 64).
|
| 32 |
+
- ``opts["dag_nodes"]`` / ``opts["dag_labels"]`` / ``opts["dag_edges"]``
|
| 33 |
+
/ ``opts["dag_thresholds"]`` / ``opts["dag_higher_is_better"]`` :
|
| 34 |
+
arguments directs de :func:`build_pipeline_dag_html`.
|
| 35 |
+
- ``opts["junctions"]`` : liste de jonctions avec leurs paires
|
| 36 |
+
``before/after`` pour :func:`build_error_absorption_html`.
|
| 37 |
+
- ``opts["incremental_runs"]`` + ``opts["incremental_varying_slot"]`` :
|
| 38 |
+
arguments de :func:`build_incremental_comparison_html`.
|
| 39 |
+
- ``opts["module_audits"]`` : liste de ``(manifest, audit_result)``.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
from __future__ import annotations
|
| 43 |
+
|
| 44 |
+
import logging
|
| 45 |
+
from typing import Any, Optional
|
| 46 |
+
|
| 47 |
+
logger = logging.getLogger(__name__)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def build_pipeline_view_html(
|
| 51 |
+
report_data: Optional[dict] = None,
|
| 52 |
+
labels: Optional[dict[str, str]] = None,
|
| 53 |
+
*,
|
| 54 |
+
pipeline_benchmark: Optional[Any] = None,
|
| 55 |
+
dag_nodes: Optional[list] = None,
|
| 56 |
+
dag_labels: Optional[dict[str, str]] = None,
|
| 57 |
+
dag_edges: Optional[list] = None,
|
| 58 |
+
dag_thresholds: Optional[tuple[float, float]] = None,
|
| 59 |
+
dag_higher_is_better: bool = False,
|
| 60 |
+
junctions: Optional[list[dict]] = None,
|
| 61 |
+
incremental_runs: Optional[list] = None,
|
| 62 |
+
incremental_varying_slot: Optional[str] = None,
|
| 63 |
+
incremental_higher_is_better: bool = False,
|
| 64 |
+
module_audits: Optional[list[tuple]] = None,
|
| 65 |
+
) -> str:
|
| 66 |
+
"""Compose la vue pipeline.
|
| 67 |
+
|
| 68 |
+
Parameters
|
| 69 |
+
----------
|
| 70 |
+
report_data:
|
| 71 |
+
Inutilisé pour cette vue (la pipeline composée a sa propre
|
| 72 |
+
structure de données via ``PipelineBenchmarkResult``).
|
| 73 |
+
Présent dans la signature pour homogénéité avec les autres
|
| 74 |
+
vues du chantier 3.
|
| 75 |
+
labels:
|
| 76 |
+
Dict i18n complet.
|
| 77 |
+
pipeline_benchmark:
|
| 78 |
+
``PipelineBenchmarkResult`` (Sprint 64) — active les sections
|
| 79 |
+
``summary`` et ``steps_table`` du :mod:`pipeline_render`.
|
| 80 |
+
dag_nodes, dag_labels, dag_edges, dag_thresholds, dag_higher_is_better:
|
| 81 |
+
Arguments de :func:`build_pipeline_dag_html` (Sprint 95).
|
| 82 |
+
junctions:
|
| 83 |
+
Liste de dicts ``{junction_name, before, after, ...}`` pour
|
| 84 |
+
:func:`build_error_absorption_html` (Sprint 94).
|
| 85 |
+
incremental_runs, incremental_varying_slot, incremental_higher_is_better:
|
| 86 |
+
Arguments de :func:`build_incremental_comparison_html`
|
| 87 |
+
(Sprint 96).
|
| 88 |
+
module_audits:
|
| 89 |
+
Liste de tuples ``(ModuleManifest, AuditResult)`` pour
|
| 90 |
+
:func:`build_module_audit_html` (Sprint 97).
|
| 91 |
+
|
| 92 |
+
Returns
|
| 93 |
+
-------
|
| 94 |
+
str
|
| 95 |
+
HTML de la vue ou ``""`` si aucune sous-section opt-in
|
| 96 |
+
n'est fournie.
|
| 97 |
+
"""
|
| 98 |
+
labels = labels or {}
|
| 99 |
+
blocks: list[tuple[str, str]] = []
|
| 100 |
+
|
| 101 |
+
# Sous-section 1 : résumé + steps table
|
| 102 |
+
if pipeline_benchmark is not None:
|
| 103 |
+
try:
|
| 104 |
+
from picarones.report.pipeline_render import (
|
| 105 |
+
build_pipeline_steps_table_html,
|
| 106 |
+
build_pipeline_summary_html,
|
| 107 |
+
)
|
| 108 |
+
summary = build_pipeline_summary_html(pipeline_benchmark)
|
| 109 |
+
steps = build_pipeline_steps_table_html(pipeline_benchmark)
|
| 110 |
+
combined = "\n".join(filter(None, [summary, steps]))
|
| 111 |
+
if combined:
|
| 112 |
+
blocks.append((
|
| 113 |
+
labels.get(
|
| 114 |
+
"pipeline_summary_title",
|
| 115 |
+
"Résumé de la pipeline",
|
| 116 |
+
),
|
| 117 |
+
combined,
|
| 118 |
+
))
|
| 119 |
+
except Exception as exc: # noqa: BLE001
|
| 120 |
+
logger.warning(
|
| 121 |
+
"[pipeline_view.summary] dégradé : %s", exc,
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
# Sous-section 2 : DAG visualization
|
| 125 |
+
if dag_nodes:
|
| 126 |
+
try:
|
| 127 |
+
from picarones.report.pipeline_dag_render import (
|
| 128 |
+
build_pipeline_dag_html,
|
| 129 |
+
)
|
| 130 |
+
html = build_pipeline_dag_html(
|
| 131 |
+
nodes=dag_nodes,
|
| 132 |
+
labels=dag_labels or {},
|
| 133 |
+
edges=dag_edges,
|
| 134 |
+
thresholds=dag_thresholds or (0.05, 0.15),
|
| 135 |
+
higher_is_better=dag_higher_is_better,
|
| 136 |
+
)
|
| 137 |
+
if html:
|
| 138 |
+
blocks.append((
|
| 139 |
+
labels.get(
|
| 140 |
+
"pipeline_dag_title",
|
| 141 |
+
"Visualisation du DAG",
|
| 142 |
+
),
|
| 143 |
+
html,
|
| 144 |
+
))
|
| 145 |
+
except Exception as exc: # noqa: BLE001
|
| 146 |
+
logger.warning("[pipeline_view.dag] dégradé : %s", exc)
|
| 147 |
+
|
| 148 |
+
# Sous-section 3 : absorption d'erreur par jonction
|
| 149 |
+
if junctions:
|
| 150 |
+
try:
|
| 151 |
+
from picarones.report.error_absorption_render import (
|
| 152 |
+
build_error_absorption_html,
|
| 153 |
+
)
|
| 154 |
+
html = build_error_absorption_html(junctions, labels=labels)
|
| 155 |
+
if html:
|
| 156 |
+
blocks.append((
|
| 157 |
+
labels.get(
|
| 158 |
+
"pipeline_absorption_title",
|
| 159 |
+
"Absorption d'erreur par jonction",
|
| 160 |
+
),
|
| 161 |
+
html,
|
| 162 |
+
))
|
| 163 |
+
except Exception as exc: # noqa: BLE001
|
| 164 |
+
logger.warning(
|
| 165 |
+
"[pipeline_view.error_absorption] dégradé : %s", exc,
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
# Sous-section 4 : comparaison incrémentale (effet d'un slot)
|
| 169 |
+
if incremental_runs and incremental_varying_slot:
|
| 170 |
+
try:
|
| 171 |
+
from picarones.core.incremental_comparison import (
|
| 172 |
+
compare_isolated_effect,
|
| 173 |
+
)
|
| 174 |
+
from picarones.report.incremental_comparison_render import (
|
| 175 |
+
build_incremental_comparison_html,
|
| 176 |
+
)
|
| 177 |
+
comparison = compare_isolated_effect(
|
| 178 |
+
incremental_runs,
|
| 179 |
+
incremental_varying_slot,
|
| 180 |
+
higher_is_better=incremental_higher_is_better,
|
| 181 |
+
)
|
| 182 |
+
html = build_incremental_comparison_html(
|
| 183 |
+
comparison,
|
| 184 |
+
varying_slot=incremental_varying_slot,
|
| 185 |
+
labels=labels,
|
| 186 |
+
)
|
| 187 |
+
if html:
|
| 188 |
+
blocks.append((
|
| 189 |
+
labels.get(
|
| 190 |
+
"pipeline_incremental_title",
|
| 191 |
+
"Comparaison incrémentale",
|
| 192 |
+
),
|
| 193 |
+
html,
|
| 194 |
+
))
|
| 195 |
+
except Exception as exc: # noqa: BLE001
|
| 196 |
+
logger.warning(
|
| 197 |
+
"[pipeline_view.incremental] dégradé : %s", exc,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
# Sous-section 5 : audit des modules contribués
|
| 201 |
+
if module_audits:
|
| 202 |
+
try:
|
| 203 |
+
from picarones.report.module_audit_render import (
|
| 204 |
+
build_module_audit_html,
|
| 205 |
+
)
|
| 206 |
+
html = build_module_audit_html(module_audits, labels=labels)
|
| 207 |
+
if html:
|
| 208 |
+
blocks.append((
|
| 209 |
+
labels.get(
|
| 210 |
+
"pipeline_audit_title",
|
| 211 |
+
"Audit des modules contribués",
|
| 212 |
+
),
|
| 213 |
+
html,
|
| 214 |
+
))
|
| 215 |
+
except Exception as exc: # noqa: BLE001
|
| 216 |
+
logger.warning("[pipeline_view.audit] dégradé : %s", exc)
|
| 217 |
+
|
| 218 |
+
if not blocks:
|
| 219 |
+
return ""
|
| 220 |
+
|
| 221 |
+
from picarones.report.views.economics import _render_view_shell
|
| 222 |
+
|
| 223 |
+
return _render_view_shell(
|
| 224 |
+
view_title=labels.get(
|
| 225 |
+
"pipeline_view_title", "Banc d'essai de pipeline composée",
|
| 226 |
+
),
|
| 227 |
+
view_note=labels.get(
|
| 228 |
+
"pipeline_view_note",
|
| 229 |
+
"Vue spécifique aux pipelines composées (axe B) : "
|
| 230 |
+
"métriques aux jonctions, absorption d'erreur, comparaison "
|
| 231 |
+
"incrémentale par slot, audit des modules contribués.",
|
| 232 |
+
),
|
| 233 |
+
blocks=blocks,
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
__all__ = ["build_pipeline_view_html"]
|
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vue robustesse — chantier 3 post-Sprint 97.
|
| 2 |
+
|
| 3 |
+
Branche le renderer :func:`picarones.report.robustness_projection_render`
|
| 4 |
+
(Sprint 88) au workflow ``picarones robustness`` (CLI Sprint 8).
|
| 5 |
+
|
| 6 |
+
Cette vue ne s'inclut pas dans le rapport classique : la robustesse
|
| 7 |
+
synthétique exige une étape de calcul lourde (re-OCR sur des
|
| 8 |
+
versions dégradées de chaque image) qui sort du flux standard.
|
| 9 |
+
Le module est exposé pour que l'orchestrateur ``robustness_cmd``
|
| 10 |
+
de la CLI puisse composer un mini-rapport HTML autonome.
|
| 11 |
+
|
| 12 |
+
Sources de données
|
| 13 |
+
------------------
|
| 14 |
+
- ``opts["projection"]`` : sortie de
|
| 15 |
+
:func:`picarones.core.robustness_projection.project_robustness_on_corpus`.
|
| 16 |
+
- ``opts["aggregated"]`` : sortie de
|
| 17 |
+
:func:`picarones.core.robustness_projection.aggregate_projection_per_engine`.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import logging
|
| 23 |
+
from typing import Any, Optional
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def build_robustness_view_html(
|
| 29 |
+
report_data: Optional[dict] = None,
|
| 30 |
+
labels: Optional[dict[str, str]] = None,
|
| 31 |
+
*,
|
| 32 |
+
projection: Optional[dict] = None,
|
| 33 |
+
aggregated: Optional[dict] = None,
|
| 34 |
+
) -> str:
|
| 35 |
+
"""Compose la vue robustesse.
|
| 36 |
+
|
| 37 |
+
Parameters
|
| 38 |
+
----------
|
| 39 |
+
report_data:
|
| 40 |
+
Inutilisé (la robustesse a son propre flux). Présent pour
|
| 41 |
+
homogénéité avec les autres vues du chantier 3.
|
| 42 |
+
labels:
|
| 43 |
+
Dict i18n complet.
|
| 44 |
+
projection:
|
| 45 |
+
Sortie de
|
| 46 |
+
:func:`picarones.core.robustness_projection.project_robustness_on_corpus`.
|
| 47 |
+
aggregated:
|
| 48 |
+
Sortie de
|
| 49 |
+
:func:`picarones.core.robustness_projection.aggregate_projection_per_engine`.
|
| 50 |
+
Si ``None`` mais ``projection`` fourni, recalculé.
|
| 51 |
+
|
| 52 |
+
Returns
|
| 53 |
+
-------
|
| 54 |
+
str
|
| 55 |
+
HTML de la vue ou ``""`` si pas de projection fournie.
|
| 56 |
+
"""
|
| 57 |
+
if projection is None:
|
| 58 |
+
return ""
|
| 59 |
+
labels = labels or {}
|
| 60 |
+
blocks: list[tuple[str, str]] = []
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
from picarones.report.robustness_projection_render import (
|
| 64 |
+
build_robustness_projection_html,
|
| 65 |
+
)
|
| 66 |
+
html = build_robustness_projection_html(
|
| 67 |
+
projection, aggregated=aggregated, labels=labels,
|
| 68 |
+
)
|
| 69 |
+
if html:
|
| 70 |
+
blocks.append((
|
| 71 |
+
labels.get(
|
| 72 |
+
"robust_view_title", "Déficit projeté de robustesse",
|
| 73 |
+
),
|
| 74 |
+
html,
|
| 75 |
+
))
|
| 76 |
+
except Exception as exc: # noqa: BLE001
|
| 77 |
+
logger.warning(
|
| 78 |
+
"[robustness_view.projection] dégradé : %s", exc,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
if not blocks:
|
| 82 |
+
return ""
|
| 83 |
+
|
| 84 |
+
from picarones.report.views.economics import _render_view_shell
|
| 85 |
+
|
| 86 |
+
return _render_view_shell(
|
| 87 |
+
view_title=labels.get(
|
| 88 |
+
"robust_view_title", "Robustesse projetée sur le corpus",
|
| 89 |
+
),
|
| 90 |
+
view_note=labels.get(
|
| 91 |
+
"robust_view_note",
|
| 92 |
+
"Projection des courbes de dégradation synthétique "
|
| 93 |
+
"(bruit, flou, rotation) sur les caractéristiques d'image "
|
| 94 |
+
"réelles du corpus. Permet d'estimer le déficit attendu "
|
| 95 |
+
"sans relancer un OCR coûteux par dégradation.",
|
| 96 |
+
),
|
| 97 |
+
blocks=blocks,
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
__all__ = ["build_robustness_view_html"]
|
|
@@ -0,0 +1,358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests des 5 vues HTML thématiques (chantier 3 post-Sprint 97).
|
| 2 |
+
|
| 3 |
+
Couvre :
|
| 4 |
+
|
| 5 |
+
- Importation et signature des 5 vues.
|
| 6 |
+
- Adaptive masking : ``""`` quand aucune sous-section n'a de signal.
|
| 7 |
+
- Rendu HTML cohérent quand les données sont fournies.
|
| 8 |
+
- Anti-injection HTML sur les noms de moteurs et libellés.
|
| 9 |
+
- Composition correcte du shell ``<details>`` (premier ouvert,
|
| 10 |
+
autres fermés).
|
| 11 |
+
- Câblage générator → vues (les variables sont passées au template).
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
from typing import Any
|
| 17 |
+
|
| 18 |
+
import pytest
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 22 |
+
# 1. Imports + signatures
|
| 23 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class TestViewsImport:
|
| 27 |
+
def test_all_views_import(self):
|
| 28 |
+
from picarones.report.views import (
|
| 29 |
+
build_advanced_taxonomy_view_html,
|
| 30 |
+
build_diagnostics_view_html,
|
| 31 |
+
build_economics_view_html,
|
| 32 |
+
build_pipeline_view_html,
|
| 33 |
+
build_robustness_view_html,
|
| 34 |
+
)
|
| 35 |
+
assert callable(build_advanced_taxonomy_view_html)
|
| 36 |
+
assert callable(build_diagnostics_view_html)
|
| 37 |
+
assert callable(build_economics_view_html)
|
| 38 |
+
assert callable(build_pipeline_view_html)
|
| 39 |
+
assert callable(build_robustness_view_html)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 43 |
+
# 2. Adaptive masking — vues vides retournent ""
|
| 44 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@pytest.fixture
|
| 48 |
+
def empty_report_data() -> dict:
|
| 49 |
+
return {"engines": []}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class TestAdaptiveMasking:
|
| 53 |
+
def test_economics_empty_returns_empty(self, empty_report_data):
|
| 54 |
+
from picarones.report.views import build_economics_view_html
|
| 55 |
+
|
| 56 |
+
assert build_economics_view_html(empty_report_data, {}) == ""
|
| 57 |
+
|
| 58 |
+
def test_advanced_taxonomy_empty_returns_empty(self, empty_report_data):
|
| 59 |
+
from picarones.report.views import build_advanced_taxonomy_view_html
|
| 60 |
+
|
| 61 |
+
assert build_advanced_taxonomy_view_html(empty_report_data, {}) == ""
|
| 62 |
+
|
| 63 |
+
def test_diagnostics_empty_returns_empty(self, empty_report_data):
|
| 64 |
+
from picarones.report.views import build_diagnostics_view_html
|
| 65 |
+
|
| 66 |
+
assert build_diagnostics_view_html(empty_report_data, {}) == ""
|
| 67 |
+
|
| 68 |
+
def test_pipeline_empty_returns_empty(self, empty_report_data):
|
| 69 |
+
from picarones.report.views import build_pipeline_view_html
|
| 70 |
+
|
| 71 |
+
assert build_pipeline_view_html(empty_report_data, {}) == ""
|
| 72 |
+
|
| 73 |
+
def test_robustness_empty_returns_empty(self, empty_report_data):
|
| 74 |
+
from picarones.report.views import build_robustness_view_html
|
| 75 |
+
|
| 76 |
+
assert build_robustness_view_html(empty_report_data, {}) == ""
|
| 77 |
+
|
| 78 |
+
def test_advanced_taxonomy_single_engine_returns_empty(self):
|
| 79 |
+
"""La comparaison nécessite ≥ 2 moteurs."""
|
| 80 |
+
from picarones.report.views import build_advanced_taxonomy_view_html
|
| 81 |
+
|
| 82 |
+
single = {"engines": [{
|
| 83 |
+
"name": "tess",
|
| 84 |
+
"aggregated_taxonomy": {"class_distribution": {"x": 10}},
|
| 85 |
+
}]}
|
| 86 |
+
# Pas de comparison possible → vue masquée
|
| 87 |
+
assert build_advanced_taxonomy_view_html(single, {}) == ""
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 91 |
+
# 3. Rendu HTML quand données fournies
|
| 92 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class _MockMetrics:
|
| 96 |
+
def __init__(self, *, cer=0.05, wer=0.1, reference_length=500):
|
| 97 |
+
self.cer = cer
|
| 98 |
+
self.wer = wer
|
| 99 |
+
self.reference_length = reference_length
|
| 100 |
+
self.error = None
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class _MockDocResult:
|
| 104 |
+
def __init__(self, duration=1.0):
|
| 105 |
+
self.engine_error = None
|
| 106 |
+
self.duration_seconds = duration
|
| 107 |
+
self.metrics = _MockMetrics()
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class _MockEngineReport:
|
| 111 |
+
def __init__(self, name, n_docs=10):
|
| 112 |
+
self.engine_name = name
|
| 113 |
+
self.document_results = [_MockDocResult() for _ in range(n_docs)]
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class TestEconomicsView:
|
| 117 |
+
def test_throughput_with_realistic_engines(self):
|
| 118 |
+
from picarones.report.views import build_economics_view_html
|
| 119 |
+
|
| 120 |
+
reports = [
|
| 121 |
+
_MockEngineReport("tesseract"),
|
| 122 |
+
_MockEngineReport("pero_ocr"),
|
| 123 |
+
]
|
| 124 |
+
html = build_economics_view_html(
|
| 125 |
+
{"engines": []}, {},
|
| 126 |
+
engine_reports=reports,
|
| 127 |
+
)
|
| 128 |
+
assert html != ""
|
| 129 |
+
# Les deux moteurs doivent apparaître dans le HTML
|
| 130 |
+
assert "tesseract" in html
|
| 131 |
+
assert "pero" in html
|
| 132 |
+
|
| 133 |
+
def test_extra_html_blocks_appended(self):
|
| 134 |
+
from picarones.report.views import build_economics_view_html
|
| 135 |
+
|
| 136 |
+
extra = ['<div class="custom">CUSTOM_BLOCK</div>']
|
| 137 |
+
html = build_economics_view_html(
|
| 138 |
+
{"engines": []},
|
| 139 |
+
{"economics_extra_title": "Coût projeté"},
|
| 140 |
+
engine_reports=[_MockEngineReport("tess")],
|
| 141 |
+
extra_html_blocks=extra,
|
| 142 |
+
)
|
| 143 |
+
assert "CUSTOM_BLOCK" in html
|
| 144 |
+
|
| 145 |
+
def test_zero_duration_excludes_engine(self):
|
| 146 |
+
"""Bench depuis cache (durations=0) ne génère pas de throughput."""
|
| 147 |
+
from picarones.report.views import build_economics_view_html
|
| 148 |
+
|
| 149 |
+
report = _MockEngineReport("cached")
|
| 150 |
+
for dr in report.document_results:
|
| 151 |
+
dr.duration_seconds = 0.0
|
| 152 |
+
html = build_economics_view_html(
|
| 153 |
+
{"engines": []}, {}, engine_reports=[report],
|
| 154 |
+
)
|
| 155 |
+
# Aucun moteur n'a de durée → vue masquée
|
| 156 |
+
assert html == ""
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class TestAdvancedTaxonomyView:
|
| 160 |
+
def test_two_engines_taxonomy_compared(self):
|
| 161 |
+
from picarones.report.views import build_advanced_taxonomy_view_html
|
| 162 |
+
|
| 163 |
+
report_data = {
|
| 164 |
+
"engines": [
|
| 165 |
+
{
|
| 166 |
+
"name": "tess", "cer": 0.05,
|
| 167 |
+
"aggregated_taxonomy": {
|
| 168 |
+
"class_distribution": {
|
| 169 |
+
"case_error": 100, "ligature_error": 50,
|
| 170 |
+
"lacuna": 30,
|
| 171 |
+
},
|
| 172 |
+
},
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"name": "pero", "cer": 0.07,
|
| 176 |
+
"aggregated_taxonomy": {
|
| 177 |
+
"class_distribution": {
|
| 178 |
+
"case_error": 30, "lacuna": 80,
|
| 179 |
+
"diacritic_error": 60,
|
| 180 |
+
},
|
| 181 |
+
},
|
| 182 |
+
},
|
| 183 |
+
],
|
| 184 |
+
}
|
| 185 |
+
html = build_advanced_taxonomy_view_html(report_data, {})
|
| 186 |
+
assert html != ""
|
| 187 |
+
# Le diagramme miroir doit nommer les 2 moteurs
|
| 188 |
+
assert "tess" in html
|
| 189 |
+
assert "pero" in html
|
| 190 |
+
|
| 191 |
+
def test_anti_injection_engine_name(self):
|
| 192 |
+
"""Un nom de moteur avec balises HTML doit être échappé."""
|
| 193 |
+
from picarones.report.views import build_advanced_taxonomy_view_html
|
| 194 |
+
|
| 195 |
+
report_data = {
|
| 196 |
+
"engines": [
|
| 197 |
+
{
|
| 198 |
+
"name": "<script>alert(1)</script>",
|
| 199 |
+
"cer": 0.05,
|
| 200 |
+
"aggregated_taxonomy": {
|
| 201 |
+
"class_distribution": {"case_error": 10},
|
| 202 |
+
},
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"name": "pero",
|
| 206 |
+
"cer": 0.07,
|
| 207 |
+
"aggregated_taxonomy": {
|
| 208 |
+
"class_distribution": {"lacuna": 10},
|
| 209 |
+
},
|
| 210 |
+
},
|
| 211 |
+
],
|
| 212 |
+
}
|
| 213 |
+
html = build_advanced_taxonomy_view_html(report_data, {})
|
| 214 |
+
# Pas de balise script non échappée
|
| 215 |
+
assert "<script>alert" not in html
|
| 216 |
+
# Mais le contenu doit être présent sous forme échappée
|
| 217 |
+
assert "<script" in html or "alert" not in html.lower()
|
| 218 |
+
|
| 219 |
+
def test_lexical_modernization_optional(self):
|
| 220 |
+
from picarones.report.views import build_advanced_taxonomy_view_html
|
| 221 |
+
|
| 222 |
+
report_data = {
|
| 223 |
+
"engines": [
|
| 224 |
+
{
|
| 225 |
+
"name": "tess", "cer": 0.05,
|
| 226 |
+
"aggregated_taxonomy": {
|
| 227 |
+
"class_distribution": {"case_error": 10},
|
| 228 |
+
},
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"name": "pero", "cer": 0.07,
|
| 232 |
+
"aggregated_taxonomy": {
|
| 233 |
+
"class_distribution": {"case_error": 5},
|
| 234 |
+
},
|
| 235 |
+
},
|
| 236 |
+
],
|
| 237 |
+
}
|
| 238 |
+
# Sans lexical_modernization, la sous-section n'apparaît pas
|
| 239 |
+
html_no = build_advanced_taxonomy_view_html(report_data, {})
|
| 240 |
+
# Avec, elle apparaît
|
| 241 |
+
lex_data = {
|
| 242 |
+
"per_token": {
|
| 243 |
+
"maistre": {
|
| 244 |
+
"n_total": 10, "n_modernized": 8,
|
| 245 |
+
"rate_modernized": 0.8,
|
| 246 |
+
"variants": [{"token": "maître", "count": 8}],
|
| 247 |
+
},
|
| 248 |
+
},
|
| 249 |
+
}
|
| 250 |
+
html_yes = build_advanced_taxonomy_view_html(
|
| 251 |
+
report_data, {}, lexical_modernization=lex_data,
|
| 252 |
+
)
|
| 253 |
+
# Au moins une section de plus
|
| 254 |
+
assert len(html_yes) > len(html_no)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
class TestDiagnosticsView:
|
| 258 |
+
def test_levers_only_when_signal(self):
|
| 259 |
+
"""detect_levers doit être appelé. Si rien ne déclenche, vue masquée."""
|
| 260 |
+
from picarones.report.views import build_diagnostics_view_html
|
| 261 |
+
|
| 262 |
+
# report_data minimal — aucun levier ne devrait se déclencher
|
| 263 |
+
empty = {"engines": []}
|
| 264 |
+
assert build_diagnostics_view_html(empty, {}) == ""
|
| 265 |
+
|
| 266 |
+
def test_image_predictive_with_qualities(self):
|
| 267 |
+
from picarones.report.views import build_diagnostics_view_html
|
| 268 |
+
|
| 269 |
+
# Liste d'image_qualities synthétiques (>= 1 doc)
|
| 270 |
+
qualities = [
|
| 271 |
+
{
|
| 272 |
+
"contrast": 0.8, "noise_level": 0.2,
|
| 273 |
+
"blur_score": 0.1, "estimated_dpi": 300,
|
| 274 |
+
"rotation_estimate": 0.5, "low_contrast_pct": 0.05,
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
"contrast": 0.6, "noise_level": 0.4,
|
| 278 |
+
"blur_score": 0.3, "estimated_dpi": 250,
|
| 279 |
+
"rotation_estimate": 1.0, "low_contrast_pct": 0.10,
|
| 280 |
+
},
|
| 281 |
+
]
|
| 282 |
+
html = build_diagnostics_view_html(
|
| 283 |
+
{"engines": []}, {}, image_qualities=qualities,
|
| 284 |
+
)
|
| 285 |
+
# La section image_predictive doit s'afficher
|
| 286 |
+
assert html != ""
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 290 |
+
# 4. Composition du shell <details>
|
| 291 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
class TestDetailsShell:
|
| 295 |
+
def test_first_block_open_others_closed(self):
|
| 296 |
+
from picarones.report.views.economics import _render_view_shell
|
| 297 |
+
|
| 298 |
+
html = _render_view_shell(
|
| 299 |
+
view_title="Test",
|
| 300 |
+
view_note="Note",
|
| 301 |
+
blocks=[("A", "<p>aaa</p>"), ("B", "<p>bbb</p>"), ("C", "<p>ccc</p>")],
|
| 302 |
+
)
|
| 303 |
+
# Le premier <details> doit être ouvert
|
| 304 |
+
details = html.split("<details")
|
| 305 |
+
assert "open" in details[1].split(">")[0]
|
| 306 |
+
# Les suivants ne doivent pas l'être
|
| 307 |
+
assert "open" not in details[2].split(">")[0]
|
| 308 |
+
assert "open" not in details[3].split(">")[0]
|
| 309 |
+
# Tous les contenus présents
|
| 310 |
+
assert "aaa" in html and "bbb" in html and "ccc" in html
|
| 311 |
+
|
| 312 |
+
def test_xml_chars_in_titles_escaped(self):
|
| 313 |
+
from picarones.report.views.economics import _render_view_shell
|
| 314 |
+
|
| 315 |
+
html = _render_view_shell(
|
| 316 |
+
view_title="<script>alert(1)</script>",
|
| 317 |
+
view_note="Note <b>bold</b>",
|
| 318 |
+
blocks=[("Block <X>", "<p>content</p>")],
|
| 319 |
+
)
|
| 320 |
+
# Pas d'injection
|
| 321 |
+
assert "<script>alert(1)</script>" not in html
|
| 322 |
+
# Mais visible sous forme échappée
|
| 323 |
+
assert "<script" in html
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 327 |
+
# 5. Câblage générator → vues
|
| 328 |
+
# ──────────────────────────────────────────────────────────────────────────
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
class TestGeneratorWiring:
|
| 332 |
+
def test_generator_imports_three_views(self):
|
| 333 |
+
"""generator.py doit importer les 3 vues automatiques (economics,
|
| 334 |
+
advanced_taxonomy, diagnostics) pour les passer au template."""
|
| 335 |
+
from pathlib import Path
|
| 336 |
+
|
| 337 |
+
gen_src = (
|
| 338 |
+
Path(__file__).parent.parent / "picarones" / "report" / "generator.py"
|
| 339 |
+
).read_text(encoding="utf-8")
|
| 340 |
+
# Les 3 imports doivent être présents
|
| 341 |
+
assert "build_economics_view_html" in gen_src
|
| 342 |
+
assert "build_advanced_taxonomy_view_html" in gen_src
|
| 343 |
+
assert "build_diagnostics_view_html" in gen_src
|
| 344 |
+
# Et les 3 variables doivent être passées au template
|
| 345 |
+
assert "economics_view_html=" in gen_src
|
| 346 |
+
assert "advanced_taxonomy_view_html=" in gen_src
|
| 347 |
+
assert "diagnostics_view_html=" in gen_src
|
| 348 |
+
|
| 349 |
+
def test_template_uses_three_views(self):
|
| 350 |
+
from pathlib import Path
|
| 351 |
+
|
| 352 |
+
tpl_src = (
|
| 353 |
+
Path(__file__).parent.parent
|
| 354 |
+
/ "picarones" / "report" / "templates" / "view_analyses.html"
|
| 355 |
+
).read_text(encoding="utf-8")
|
| 356 |
+
assert "{% if economics_view_html %}" in tpl_src
|
| 357 |
+
assert "{% if advanced_taxonomy_view_html %}" in tpl_src
|
| 358 |
+
assert "{% if diagnostics_view_html %}" in tpl_src
|