feat(phase2): Gazetteers INSEE (36K prénoms + 34K communes) + silver annotations

- Prénoms INSEE renforcent la confiance NER (prénom connu → ne pas filtrer)
- Communes INSEE disponibles pour distinction ville/nom de famille
- Export 29 fichiers silver annotations (252K tokens, 12.8K entités) pour fine-tuning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 12:03:17 +01:00
parent 26ac02b0cb
commit 6e0e8c7312
32 changed files with 322066 additions and 3 deletions

View File

@@ -114,6 +114,42 @@ def _load_bdpm_medication_names() -> set:
return set()
# ----------------- Gazetteers INSEE (prénoms + communes) -----------------
_INSEE_PRENOMS: set = set()
_INSEE_COMMUNES: set = set()
def _load_insee_gazetteers():
"""Charge les gazetteers INSEE (prénoms français + communes)."""
global _INSEE_PRENOMS, _INSEE_COMMUNES
data_dir = Path(__file__).parent / "data" / "insee"
# Prénoms (lowercase, >= 3 chars)
prenoms_path = data_dir / "prenoms_france.txt"
if prenoms_path.exists():
try:
_INSEE_PRENOMS = {
line.strip().lower() for line in prenoms_path.read_text(encoding="utf-8").splitlines()
if line.strip() and len(line.strip()) >= 3
}
log.info(f"Gazetteers INSEE prénoms: {len(_INSEE_PRENOMS)} entrées")
except Exception as e:
log.warning(f"Erreur chargement prénoms INSEE: {e}")
# Communes (uppercase, >= 3 chars)
communes_path = data_dir / "communes_france.txt"
if communes_path.exists():
try:
_INSEE_COMMUNES = {
line.strip().upper() for line in communes_path.read_text(encoding="utf-8").splitlines()
if line.strip() and len(line.strip()) >= 3
}
log.info(f"Gazetteers INSEE communes: {len(_INSEE_COMMUNES)} entrées")
except Exception as e:
log.warning(f"Erreur chargement communes INSEE: {e}")
_load_insee_gazetteers()
# ----------------- Whitelists Médicales -----------------
_MEDICAL_STRUCTURAL_TERMS = set()
_MEDICATION_WHITELIST = set()
@@ -1846,14 +1882,16 @@ def _mask_with_eds_pseudo(text: str, ents: List[Dict[str, Any]], cfg: Dict[str,
# Vérifier si c'est un médicament connu
if w.lower() in _MEDICATION_WHITELIST:
continue
# Chantier 3+4 : Confiance NER + vote croisé GLiNER (combinés)
# Chantier 3+4 : Confiance NER + vote croisé GLiNER + gazetteers INSEE
# Sécurité d'abord : haute confiance NER → toujours masquer
# GLiNER peut rejeter SEULEMENT si confiance NER basse
gliner_vote = e.get("gliner_confirmed") # True=PII, False=médical, None=neutre
if label in ("NOM", "PRENOM"):
score = e.get("score", 1.0)
if isinstance(score, float) and score < 0.70:
# Basse confiance NER : GLiNER peut trancher
# Gazetteer INSEE : prénom connu = renforcement confiance (ne pas filtrer)
is_known_prenom = w.lower() in _INSEE_PRENOMS
if isinstance(score, float) and score < 0.70 and not is_known_prenom:
# Basse confiance NER + pas un prénom connu : GLiNER peut trancher
if gliner_vote is False:
continue # NER pas sûr + GLiNER dit "médical" → skip
if score < 0.30: