Spaces:
Sleeping
Sleeping
File size: 6,571 Bytes
11b4df4 974df5a 11b4df4 974df5a 11b4df4 974df5a 11b4df4 974df5a | 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 | """Score de difficulté intrinsèque par document.
Le score est indépendant des moteurs OCR : il mesure la difficulté
*objective* d'un document, indépendamment de la qualité des transcriptions.
Formule
-------
difficulty = w_variance * variance_norm
+ w_quality * (1 - image_quality_score)
+ w_density * special_char_density
où :
- variance_norm : variance inter-moteurs du CER, normalisée [0, 1]
- image_quality : score de qualité image [0, 1] (netteté, contraste…)
- special_chars : densité de caractères spéciaux dans la GT [0, 1]
Les poids sont configurables (défaut : 0.4 / 0.35 / 0.25).
Score final : [0, 1] — 0 = document facile, 1 = très difficile.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Optional
# Poids par défaut
_W_VARIANCE = 0.40
_W_QUALITY = 0.35
_W_DENSITY = 0.25
# Caractères spéciaux patrimoniaux (ligatures, abréviations, diacritiques rares)
_SPECIAL_CHARS_RE = re.compile(
r"[ſœæꝑꝓ&]" # ligatures / abréviations médiévales
r"|[ḁ-ỿ]" # Latin Étendu Additionnel (diacritiques rares)
r"|[\u0300-\u036f]" # Diacritiques combinants
r"|[\ufb00-\ufb06]" # Formes de présentation latines (fi, fl…)
r"|[IVXLCDM]{3,}" # Chiffres romains (3+ caractères)
)
@dataclass
class DifficultyScore:
"""Score de difficulté intrinsèque d'un document."""
doc_id: str
score: float
"""Score global [0, 1] — plus élevé = plus difficile."""
variance_component: float
"""Composante variance inter-moteurs [0, 1]."""
quality_component: float
"""Composante qualité image inversée [0, 1]."""
density_component: float
"""Composante densité caractères spéciaux [0, 1]."""
cer_variance: float
"""Variance brute du CER entre moteurs."""
image_quality_score: float
"""Score de qualité image (si disponible, sinon 0.5)."""
special_char_ratio: float
"""Ratio caractères spéciaux / longueur GT."""
def as_dict(self) -> dict:
return {
"doc_id": self.doc_id,
"score": round(self.score, 4),
"variance_component": round(self.variance_component, 4),
"quality_component": round(self.quality_component, 4),
"density_component": round(self.density_component, 4),
"cer_variance": round(self.cer_variance, 6),
"image_quality_score": round(self.image_quality_score, 4),
"special_char_ratio": round(self.special_char_ratio, 4),
}
def _special_char_density(text: str) -> float:
"""Ratio de caractères spéciaux patrimoniaux dans le texte."""
if not text:
return 0.0
matches = len(_SPECIAL_CHARS_RE.findall(text))
return min(1.0, matches / len(text))
def _variance(values: list[float]) -> float:
"""Variance d'une liste de valeurs."""
if len(values) < 2:
return 0.0
mu = sum(values) / len(values)
return sum((v - mu) ** 2 for v in values) / len(values)
def compute_difficulty_score(
doc_id: str,
ground_truth: str,
cer_per_engine: list[float],
image_quality_score: Optional[float] = None,
weights: tuple[float, float, float] = (_W_VARIANCE, _W_QUALITY, _W_DENSITY),
) -> DifficultyScore:
"""Calcule le score de difficulté intrinsèque pour un document.
Parameters
----------
doc_id : identifiant du document
ground_truth : texte de référence
cer_per_engine : liste des CER (un par moteur concurrent)
image_quality_score: score de qualité image [0, 1] (None → 0.5 neutre)
weights : (w_variance, w_quality, w_density)
Returns
-------
DifficultyScore
"""
w_var, w_qual, w_den = weights
# 1. Variance inter-moteurs (normalisée sur [0, 1] — variance max ≈ 0.25)
cer_var = _variance(cer_per_engine)
variance_norm = min(1.0, cer_var / 0.25)
# 2. Qualité image inversée
iq = image_quality_score if image_quality_score is not None else 0.5
iq = max(0.0, min(1.0, iq))
quality_component = 1.0 - iq
# 3. Densité de caractères spéciaux
density = _special_char_density(ground_truth)
# Amplifier légèrement (la densité brute est souvent faible)
density_component = min(1.0, density * 3.0)
# Score combiné
score = (
w_var * variance_norm
+ w_qual * quality_component
+ w_den * density_component
)
score = max(0.0, min(1.0, score))
return DifficultyScore(
doc_id=doc_id,
score=score,
variance_component=variance_norm,
quality_component=quality_component,
density_component=density_component,
cer_variance=cer_var,
image_quality_score=iq,
special_char_ratio=density,
)
def compute_all_difficulties(
doc_ids: list[str],
ground_truths: dict[str, str],
cer_map: dict[str, dict[str, float]],
image_quality_map: Optional[dict[str, float]] = None,
) -> dict[str, DifficultyScore]:
"""Calcule les scores de difficulté pour tous les documents d'un corpus.
Parameters
----------
doc_ids : liste des identifiants de documents
ground_truths : {doc_id → gt_text}
cer_map : {doc_id → {engine_name → cer}}
image_quality_map : {doc_id → quality_score} (facultatif)
Returns
-------
{doc_id → DifficultyScore}
"""
result = {}
for doc_id in doc_ids:
gt = ground_truths.get(doc_id, "")
engine_cers = list(cer_map.get(doc_id, {}).values())
iq = (image_quality_map or {}).get(doc_id)
result[doc_id] = compute_difficulty_score(
doc_id=doc_id,
ground_truth=gt,
cer_per_engine=engine_cers,
image_quality_score=iq,
)
return result
def difficulty_label(score: float) -> str:
"""Retourne un label lisible pour un score de difficulté."""
if score < 0.25:
return "Facile"
if score < 0.50:
return "Modéré"
if score < 0.75:
return "Difficile"
return "Très difficile"
def difficulty_color(score: float) -> str:
"""Retourne une couleur CSS pour un score de difficulté."""
from picarones.core.colors import COLOR_GREEN, COLOR_YELLOW, COLOR_ORANGE, COLOR_RED
if score < 0.25:
return COLOR_GREEN
if score < 0.50:
return COLOR_YELLOW
if score < 0.75:
return COLOR_ORANGE
return COLOR_RED
|