File size: 9,071 Bytes
fe6661c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979f3c3
fe6661c
979f3c3
fe6661c
979f3c3
fe6661c
 
 
 
 
 
 
 
 
 
 
c0f7ba9
fe6661c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979f3c3
fe6661c
 
 
979f3c3
fe6661c
 
 
979f3c3
fe6661c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979f3c3
fe6661c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
"""Vue taxonomique avancée — chantier 3 post-Sprint 97.

Regroupe les renderers orientés *édition critique* qui examinent la
structure des erreurs OCR au-delà du CER global :

- :func:`picarones.report.taxonomy_comparison_render.build_taxonomy_comparison_html`
  — diagramme miroir A vs B des proportions d'erreurs par classe
  + tableau de récupérabilité éditoriale.
- :func:`picarones.report.taxonomy_cooccurrence_render.build_taxonomy_cooccurrence_html`
  — heatmap Jaccard des co-occurrences de classes au niveau document
  (opt-in : nécessite ``per_doc_classes``).
- :func:`picarones.report.taxonomy_intra_doc_render.build_taxonomy_intra_doc_html`
  — heatmap classe × position intra-document (opt-in : nécessite des
  paires gt+hyp non compactées).
- :func:`picarones.report.lexical_modernization_render.build_lexical_modernization_html`
  — top-N des tokens GT modernisés par le moteur (opt-in :
  nécessite la sortie de ``compute_lexical_modernization``).

Sources de données automatiques
-------------------------------
- *Comparaison* : utilise ``aggregated_taxonomy.class_distribution``
  (ou ``counts``) du leader CER vs le runner-up. Disponible dès qu'au
  moins 2 moteurs ont une taxonomie agrégée.

Sources de données opt-in (via ``opts``)
----------------------------------------
- ``opts["cooccurrence"]``      : sortie de
  :func:`picarones.measurements.taxonomy_cooccurrence.compute_taxonomy_cooccurrence`.
- ``opts["intra_doc"]``         : sortie de
  :func:`picarones.measurements.taxonomy_intra_doc.compute_taxonomy_position_heatmap`.
- ``opts["lexical_modernization"]``  : sortie de
  :func:`picarones.measurements.lexical_modernization.compute_lexical_modernization`
  agrégée corpus-wide.

Ces calculs ne sont pas faits automatiquement par le runner standard
(coût et données nécessaires non triviaux après ``compact()``) ;
l'utilisateur peut les pré-calculer dans son workflow et les
fournir via :func:`build_advanced_taxonomy_view_html`.
"""

from __future__ import annotations

import logging
from typing import Optional

logger = logging.getLogger(__name__)


def _select_two_engines_for_comparison(
    engines_summary: list[dict],
) -> Optional[tuple[dict, dict]]:
    """Choisit deux moteurs à comparer dans le diagramme miroir.

    Stratégie : leader CER (plus bas) vs runner-up (deuxième). Si
    moins de 2 moteurs ont une ``aggregated_taxonomy`` non vide,
    retourne ``None``.
    """
    candidates = [
        e for e in engines_summary
        if isinstance(e.get("aggregated_taxonomy"), dict)
        and (
            e["aggregated_taxonomy"].get("class_distribution")
            or e["aggregated_taxonomy"].get("counts")
        )
    ]
    if len(candidates) < 2:
        return None
    # Tri par CER croissant (leader = meilleur). Les moteurs sans CER
    # vont en queue (clé None considérée comme inf).
    candidates.sort(
        key=lambda e: e.get("cer") if e.get("cer") is not None else float("inf"),
    )
    return candidates[0], candidates[1]


def _extract_class_counts(engine_entry: dict) -> dict[str, float]:
    """Extrait le dict ``{class_name: count}`` d'une entrée moteur.

    Supporte les deux formats observés en production :

    - Sprint 5 historique : ``aggregated_taxonomy["class_distribution"]``
    - Variante : ``aggregated_taxonomy["counts"]``
    """
    tax = engine_entry.get("aggregated_taxonomy") or {}
    counts = tax.get("class_distribution") or tax.get("counts") or {}
    if not isinstance(counts, dict):
        return {}
    out: dict[str, float] = {}
    for k, v in counts.items():
        if isinstance(v, (int, float)) and v >= 0:
            out[str(k)] = float(v)
    return out


def build_advanced_taxonomy_view_html(
    report_data: dict,
    labels: Optional[dict[str, str]] = None,
    *,
    cooccurrence: Optional[dict] = None,
    intra_doc: Optional[dict] = None,
    lexical_modernization: Optional[dict] = None,
) -> str:
    """Compose la vue taxonomique avancée du rapport.

    Parameters
    ----------
    report_data:
        Dict produit par :func:`generator._build_report_data`.
    labels:
        Dict i18n complet.
    cooccurrence:
        Sortie pré-calculée de
        :func:`picarones.measurements.taxonomy_cooccurrence.compute_taxonomy_cooccurrence`.
        Optionnel — la sous-section est masquée si non fourni.
    intra_doc:
        Sortie pré-calculée de
        :func:`picarones.measurements.taxonomy_intra_doc.compute_taxonomy_position_heatmap`.
        Optionnel.
    lexical_modernization:
        Sortie pré-calculée de
        :func:`picarones.measurements.lexical_modernization.aggregate_lexical_modernization`.
        Optionnel.

    Returns
    -------
    str
        HTML de la vue (entête + sous-sections collapsibles) ou
        ``""`` si aucune sous-section n'a de contenu.
    """
    labels = labels or {}
    blocks: list[tuple[str, str]] = []

    # Sous-section 1 : comparaison des deux leaders
    try:
        engines_summary = report_data.get("engines") or []
        pair = _select_two_engines_for_comparison(engines_summary)
        if pair is not None:
            from picarones.measurements.taxonomy_comparison import compare_taxonomies
            from picarones.report.taxonomy_comparison_render import (
                build_taxonomy_comparison_html,
            )
            engine_a, engine_b = pair
            data = compare_taxonomies(
                engine_a.get("name", "engine_a"),
                _extract_class_counts(engine_a),
                engine_b.get("name", "engine_b"),
                _extract_class_counts(engine_b),
            )
            html = build_taxonomy_comparison_html(data, labels=labels)
            if html:
                blocks.append((
                    labels.get(
                        "advtax_comparison_title",
                        "Comparaison taxonomique (leader vs runner-up)",
                    ),
                    html,
                ))
    except Exception as exc:  # noqa: BLE001
        logger.warning(
            "[advanced_taxonomy_view.comparison] dégradé : %s", exc,
        )

    # Sous-section 2 : co-occurrence (opt-in)
    if cooccurrence:
        try:
            from picarones.report.taxonomy_cooccurrence_render import (
                build_taxonomy_cooccurrence_html,
            )
            html = build_taxonomy_cooccurrence_html(cooccurrence, labels=labels)
            if html:
                blocks.append((
                    labels.get(
                        "advtax_cooccurrence_title",
                        "Co-occurrence de classes d'erreurs",
                    ),
                    html,
                ))
        except Exception as exc:  # noqa: BLE001
            logger.warning(
                "[advanced_taxonomy_view.cooccurrence] dégradé : %s", exc,
            )

    # Sous-section 3 : intra-document (opt-in)
    if intra_doc:
        try:
            from picarones.report.taxonomy_intra_doc_render import (
                build_taxonomy_intra_doc_html,
            )
            html = build_taxonomy_intra_doc_html(intra_doc, labels=labels)
            if html:
                blocks.append((
                    labels.get(
                        "advtax_intra_doc_title",
                        "Distribution intra-document des classes",
                    ),
                    html,
                ))
        except Exception as exc:  # noqa: BLE001
            logger.warning(
                "[advanced_taxonomy_view.intra_doc] dégradé : %s", exc,
            )

    # Sous-section 4 : modernisation lexicale (opt-in)
    if lexical_modernization:
        try:
            from picarones.report.lexical_modernization_render import (
                build_lexical_modernization_html,
            )
            html = build_lexical_modernization_html(
                lexical_modernization, labels=labels,
            )
            if html:
                blocks.append((
                    labels.get(
                        "advtax_lexmod_title",
                        "Modernisation lexicale (top tokens)",
                    ),
                    html,
                ))
        except Exception as exc:  # noqa: BLE001
            logger.warning(
                "[advanced_taxonomy_view.lexmod] dégradé : %s", exc,
            )

    if not blocks:
        return ""

    # Réutilise le shell partagé de la vue economics
    from picarones.report.views.economics import _render_view_shell

    return _render_view_shell(
        view_title=labels.get(
            "advtax_view_title", "Taxonomie avancée des erreurs",
        ),
        view_note=labels.get(
            "advtax_view_note",
            "Vue centrée sur l'édition critique : composition des "
            "erreurs au-delà du CER global, pour décider quel moteur "
            "produit des erreurs récupérables vs irrécupérables.",
        ),
        blocks=blocks,
    )


__all__ = ["build_advanced_taxonomy_view_html"]