feat(extract): normaliser ghs_injustifie en 0/1 (P2)

Qwen renvoie typiquement le libellé complet `0 SE 1 2 3 4 ATU FFM FSD`
dans le champ ghs_injustifie alors qu'une seule valeur 0/1 est attendue.
Ajout de `pipeline.checkboxes.parse_ghs_injustifie` qui extrait le
premier chiffre 0/1 via regex, ou "" si illisible.

Post-traitement appliqué à chaque extraction recueil et aux 18 JSONs
V2 existants (10 fichiers corrigés en place — les 8 autres avaient
déjà ghs_injustifie absent ou vide).

Note sur les 7 cases SE1-4/ATU/FFM/FSD : zones trop petites pour être
calibrées à l'œil et aucun cas positif (`ghs_injustifie=1`) dans
l'échantillon 2018 pour valider visuellement. La détection est en
placeholder, à recalibrer sur un cas positif réel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-24 15:54:16 +02:00
parent 7dc3eba1fc
commit 7d45018139
12 changed files with 152 additions and 14 deletions

View File

@@ -39,6 +39,14 @@ CONCERTATION_2_DECISION = CheckboxZones(
desaccord= (0.280, 0.270, 0.305, 0.290), # retour groupage DIM
)
# Zones des 7 cases SE 1 / 2 / 3 / 4 / ATU / FFM / FSD (page recueil, en bas).
# TODO : recalibrer avec des vrais cas positifs — sur 18 dossiers de
# l'échantillon 2018, aucune case n'est cochée (`ghs_injustifie = 0` partout)
# donc impossible de valider visuellement la détection. Laissé désactivé.
GHS_INJUSTIFIE_CHECKBOXES: dict[str, tuple[float, float, float, float]] = {
# placeholder — à recalibrer quand un cas positif sera observé
}
def dark_ratio(image: Image.Image, zone: tuple[float, float, float, float],
inner_frac: float = INNER_FRAC) -> float:
@@ -58,6 +66,31 @@ def dark_ratio(image: Image.Image, zone: tuple[float, float, float, float],
return float(np.mean(gray < DARK_THRESHOLD))
def parse_ghs_injustifie(raw: str) -> str:
"""Extrait la valeur 0/1 du champ ghs_injustifie depuis la sortie OCR brute.
Qwen tend à recopier le libellé complet `0 SE 1 2 3 4 ATU FFM FSD` au lieu
du seul chiffre. On prend le premier caractère qui est 0 ou 1 et on ignore
le reste (les chiffres 1/2/3/4 qui suivent « SE » sont des numéros de case,
pas la valeur du flag).
"""
if raw is None:
return ""
s = str(raw).strip()
if not s:
return ""
# Si déjà propre (juste "0" ou "1"), retour direct
if s in ("0", "1"):
return s
# Prendre le premier chiffre trouvé qui soit 0 ou 1, en ignorant tout
# le reste (en particulier les "SE 1 2 3 4…" qui suivent)
import re as _re
m = _re.match(r"\s*([01])\b", s)
if m:
return m.group(1)
return "" # illisible / format inattendu
def detect_accord_desaccord(
image_path: str | Path,
zones: CheckboxZones = RECUEIL_ACCORD_DESACCORD,