File size: 6,014 Bytes
ad8d926
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Métriques natives enregistrées dans le registre typé (Sprint 34).

Ce module est un démonstrateur d'enregistrement : il expose les
métriques scalaires existantes (CER, WER, MER, WIL) sous une forme
unitaire dans le registre, plus un stub typé hétérogène pour les
jonctions ``(TEXT, ALTO)``.

L'import du module suffit à peupler le registre — le décorateur
``@register_metric`` s'exécute à l'import.  Les sprints suivants (axe A
du plan d'évolution) ajouteront ici les métriques structurelles
(``reading_order_f1``, ``layout_f1``), philologiques (``unicode_block_*``,
``mufi_coverage``), et de fiabilité (``ece``, ``mce``).

Important — pas de double calcul
-------------------------------
Ces wrappers ne **remplacent pas** ``compute_metrics`` du module
``metrics.py``.  Ils existent pour les nouveaux chemins (pipelines
composées qui calculent par jonction).  Le rapport HTML existant
continue à passer par ``compute_metrics`` et reste donc strictement
identique octet par octet (critère de la Phase 0.3).
"""

from __future__ import annotations

import logging

from picarones.evaluation.metric_registry import register_metric
from picarones.domain.artifacts import ArtifactType

logger = logging.getLogger(__name__)


try:
    import jiwer
    _JIWER_AVAILABLE = True
except ImportError:
    _JIWER_AVAILABLE = False


# ──────────────────────────────────────────────────────────────────────────
# Métriques scalaires (TEXT, TEXT) — wrappers fins autour de jiwer
# ──────────────────────────────────────────────────────────────────────────


def _safe_jiwer_call(fn, reference: str, hypothesis: str) -> float:
    """Wrapper qui gère les cas dégénérés (références ou hypothèses vides)."""
    if not _JIWER_AVAILABLE:
        raise RuntimeError(
            "jiwer n'est pas installé — installer avec `pip install jiwer`"
        )
    if not reference:
        return 0.0 if not hypothesis else 1.0
    if not hypothesis:
        return 1.0
    return fn(reference, hypothesis)


@register_metric(
    name="cer",
    input_types=(ArtifactType.TEXT, ArtifactType.TEXT),
    description="Character Error Rate (distance d'édition normalisée par la longueur de la GT).",
    higher_is_better=False,
    tags={"text", "edit_distance", "error_rate"},
)
def cer(reference: str, hypothesis: str) -> float:
    """CER brut sur les caractères, via jiwer."""
    return _safe_jiwer_call(jiwer.cer, reference, hypothesis)


@register_metric(
    name="wer",
    input_types=(ArtifactType.TEXT, ArtifactType.TEXT),
    description="Word Error Rate.",
    higher_is_better=False,
    tags={"text", "edit_distance", "error_rate"},
)
def wer(reference: str, hypothesis: str) -> float:
    """WER brut, via jiwer."""
    return _safe_jiwer_call(jiwer.wer, reference, hypothesis)


@register_metric(
    name="mer",
    input_types=(ArtifactType.TEXT, ArtifactType.TEXT),
    description="Match Error Rate (jiwer).",
    higher_is_better=False,
    tags={"text", "error_rate"},
)
def mer(reference: str, hypothesis: str) -> float:
    return _safe_jiwer_call(jiwer.mer, reference, hypothesis)


@register_metric(
    name="wil",
    input_types=(ArtifactType.TEXT, ArtifactType.TEXT),
    description="Word Information Lost (jiwer).",
    higher_is_better=False,
    tags={"text", "error_rate"},
)
def wil(reference: str, hypothesis: str) -> float:
    return _safe_jiwer_call(jiwer.wil, reference, hypothesis)


# ──────────────────────────────────────────────────────────────────────────
# Métrique typée hétérogène (TEXT, ALTO) — stub démonstrateur
# ──────────────────────────────────────────────────────────────────────────


@register_metric(
    name="text_preservation_after_reconstruction",
    input_types=(ArtifactType.TEXT, ArtifactType.ALTO),
    description=(
        "Taux de tokens de la GT texte présents dans le texte extrait de "
        "l'ALTO produit (preuve de concept ; remplaçable par une mesure "
        "alignée par les sprints futurs)."
    ),
    higher_is_better=True,
    tags={"structure", "preservation", "stub"},
)
def text_preservation_after_reconstruction(
    reference_text: str,
    hypothesis_alto: str,
) -> float:
    """Stub démonstrateur d'une jonction texte → ALTO.

    Sprints à venir (axe A du plan d'évolution) remplaceront cette
    implémentation par une vraie mesure de préservation : extraction
    structurée du texte ALTO via le parser dédié, alignement, calcul
    déterministe.  Pour l'instant la mesure est volontairement simple
    pour démontrer le mécanisme.

    Parameters
    ----------
    reference_text:
        Texte GT (niveau ``ArtifactType.RAW_TEXT``).
    hypothesis_alto:
        ALTO XML brut produit par un module de reconstruction (niveau
        ``ArtifactType.ALTO``).

    Returns
    -------
    float
        Taux de tokens uniques de ``reference_text`` apparaissant dans
        ``hypothesis_alto`` (case-insensitive).  ``1.0`` = tous les
        tokens préservés.
    """
    if not reference_text:
        return 1.0
    ref_tokens = {tok.lower() for tok in reference_text.split() if tok}
    if not ref_tokens:
        return 1.0
    alto_text = hypothesis_alto.lower()
    preserved = sum(1 for tok in ref_tokens if tok in alto_text)
    return preserved / len(ref_tokens)


__all__ = [
    "cer",
    "wer",
    "mer",
    "wil",
    "text_preservation_after_reconstruction",
]