fix(frozen): data/*.txt dans bundle, feedback UI pendant chargement modèles

Plantages signalés sous Windows : causes identifiées et corrigées.

1. anonymisation_onefile.spec : les fichiers data/stopwords_manuels.txt,
   villes_blacklist.txt, dpi_labels_blacklist.txt, companion_blacklist.txt
   n'étaient PAS inclus dans le bundle PyInstaller (seuls les sous-dossiers
   data/bdpm, data/finess, data/insee l'étaient). Résultat en frozen : sets
   vides, qualité dégradée, plus de faux positifs.

2. anonymizer_core_refactored_onnx.py : chargements robustifiés.
   - Helper _load_txt_set avec try/except et logging WARNING si fichier absent
   - Fallbacks intégrés (_DPI_LABELS_FALLBACK, _COMPANION_BLACKLIST_FALLBACK)
     pour continuer à fonctionner si bundle partiel
   - try/except sur stopwords_manuels.txt, villes_blacklist.txt, BDPM

3. launcher.py : UX repensée pour le chargement des modèles.
   - SetupWindow (premier lancement) : auto-démarrage (plus de clic nécessaire),
     progress bar avec étapes visuelles (/✓/✗ par modèle), bouton relance si
     échec, bouton "continuer malgré tout" pour modèles optionnels.
   - Splash screen ajouté dans launch_gui() : le chargement des gazetteers
     (INSEE 200k+ noms, FINESS 100k+ établissements) prend 15-30 s au démarrage
     normal. Sans feedback, l'utilisateur croyait l'app plantée. Le splash
     tourne pendant l'import (thread séparé, poll avec splash.after).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 08:50:42 +02:00
parent 4b5925306e
commit 8e458c16ca
3 changed files with 293 additions and 84 deletions

View File

@@ -251,11 +251,16 @@ _VILLE_BLACKLIST = {
# Enrichissement depuis fichier externe (modifiable sans toucher au code)
_villes_bl_file = Path(__file__).parent / "data" / "villes_blacklist.txt"
if _villes_bl_file.exists():
for _line in _villes_bl_file.read_text(encoding="utf-8").splitlines():
_w = _line.strip()
if _w and not _w.startswith("#"):
_VILLE_BLACKLIST.add(_w)
log.info("Villes blacklist chargées : %d entrées", len(_VILLE_BLACKLIST))
try:
for _line in _villes_bl_file.read_text(encoding="utf-8").splitlines():
_w = _line.strip()
if _w and not _w.startswith("#"):
_VILLE_BLACKLIST.add(_w)
log.info("Villes blacklist chargées : %d entrées", len(_VILLE_BLACKLIST))
except Exception as _exc:
log.error("Villes blacklist : erreur de lecture %s%s", _villes_bl_file, _exc)
else:
log.warning("Villes blacklist : fichier introuvable %s — défauts intégrés utilisés", _villes_bl_file)
try:
import ahocorasick as _ahocorasick
@@ -827,24 +832,34 @@ _MEDICAL_STOP_WORDS_SET.update(_load_edsnlp_drug_names())
# Enrichissement depuis fichier externe (modifiable sans toucher au code)
_stopwords_file = Path(__file__).parent / "data" / "stopwords_manuels.txt"
if _stopwords_file.exists():
_sw_count = 0
for _line in _stopwords_file.read_text(encoding="utf-8").splitlines():
_w = _line.strip()
if _w and not _w.startswith("#"):
_MEDICAL_STOP_WORDS_SET.add(_w)
_sw_count += 1
log.info("Stop-words manuels chargés : %d mots depuis %s", _sw_count, _stopwords_file.name)
try:
_sw_count = 0
for _line in _stopwords_file.read_text(encoding="utf-8").splitlines():
_w = _line.strip()
if _w and not _w.startswith("#"):
_MEDICAL_STOP_WORDS_SET.add(_w)
_sw_count += 1
log.info("Stop-words manuels chargés : %d mots depuis %s", _sw_count, _stopwords_file.name)
except Exception as _exc:
log.error("Stop-words manuels : erreur de lecture %s%s", _stopwords_file, _exc)
else:
log.warning("Stop-words manuels : fichier introuvable %s — qualité dégradée", _stopwords_file)
# Enrichissement BDPM : ~7300 noms commerciaux + DCI/substances actives
_bdpm_path = Path(__file__).parent / "data" / "bdpm" / "medicaments_stopwords.txt"
if _bdpm_path.exists():
_bdpm_count = 0
for _line in _bdpm_path.read_text(encoding="utf-8").splitlines():
_w = _line.strip()
if _w and not _w.startswith("#"):
_MEDICAL_STOP_WORDS_SET.add(_w)
_bdpm_count += 1
log.info("BDPM stop-words chargés : %d mots", _bdpm_count)
try:
_bdpm_count = 0
for _line in _bdpm_path.read_text(encoding="utf-8").splitlines():
_w = _line.strip()
if _w and not _w.startswith("#"):
_MEDICAL_STOP_WORDS_SET.add(_w)
_bdpm_count += 1
log.info("BDPM stop-words chargés : %d mots", _bdpm_count)
except Exception as _exc:
log.error("BDPM stop-words : erreur de lecture %s%s", _bdpm_path, _exc)
else:
log.warning("BDPM stop-words : fichier introuvable %s — qualité dégradée", _bdpm_path)
_MEDICAL_STOP_WORDS = (
r"(?:" + "|".join(re.escape(w) for w in _MEDICAL_STOP_WORDS_SET) + r")"
@@ -1133,30 +1148,60 @@ class NameCandidate:
_WHITELIST_NEVER_MASK_TOKENS: set = set()
_WHITELIST_NEVER_MASK_PHRASES: set = set()
# Safe-guards pour les défauts intégrés quand les fichiers data/*.txt sont absents
# (mode frozen où le bundle aurait omis de les inclure). Contenu minimal pour
# garantir un comportement de masquage correct même en mode dégradé.
_DPI_LABELS_FALLBACK = {
"date", "note", "heure", "type", "soin", "soins", "surv",
"page", "presc", "saint", "sainte",
}
_COMPANION_BLACKLIST_FALLBACK = {
"CANCEROLOGIE", "ONCOLOGIE", "REANIMATION", "RADIOLOGIE",
"CARDIOLOGIE", "NEUROLOGIE", "PNEUMOLOGIE", "UROLOGIE",
"MEDECINE", "DOSSIER", "CONTENTION", "ISOLEMENT", "ELIMINATION",
"ZONE", "PARTI", "PLAN", "MAIN", "FORT", "FORTE",
}
def _load_txt_set(path: Path, transform=str.lower, label: str = "file") -> set:
"""Charge un fichier .txt ligne par ligne. Robuste aux erreurs (frozen exe)."""
result: set = set()
if not path.exists():
log.warning("%s introuvable : %s — utilisation des défauts intégrés", label, path)
return result
try:
for _line in path.read_text(encoding="utf-8").splitlines():
_w = _line.strip()
if _w and not _w.startswith("#"):
result.add(transform(_w))
log.info("%s chargé : %d entrées depuis %s", label, len(result), path.name)
except Exception as exc:
log.error("%s : erreur de lecture %s%s", label, path, exc)
return result
# Labels DPI structurels à ne JAMAIS masquer comme noms (Date, Note, Heure...)
# Stocké en LOWERCASE — la comparaison est case-insensitive.
# Chargé depuis data/dpi_labels_blacklist.txt + cfg["additional_dpi_labels"].
_DPI_LABELS_SET: set = set()
_dpi_file = Path(__file__).parent / "data" / "dpi_labels_blacklist.txt"
if _dpi_file.exists():
for _line in _dpi_file.read_text(encoding="utf-8").splitlines():
_w = _line.strip()
if _w and not _w.startswith("#"):
_DPI_LABELS_SET.add(_w.lower())
log.info("DPI labels blacklist chargés : %d entrées", len(_DPI_LABELS_SET))
_DPI_LABELS_SET: set = _load_txt_set(
Path(__file__).parent / "data" / "dpi_labels_blacklist.txt",
transform=str.lower,
label="DPI labels blacklist",
)
if not _DPI_LABELS_SET:
_DPI_LABELS_SET = set(_DPI_LABELS_FALLBACK)
# Companion blacklist : termes EN MAJUSCULES qui ne sont JAMAIS des noms
# (spécialités, labos pharma, mots courants ambigus).
# Stocké en UPPERCASE — la comparaison est faite contre des candidats déjà uppercase.
# Chargé depuis data/companion_blacklist.txt + cfg["additional_companion_blacklist"].
_COMPANION_BLACKLIST_SET: set = set()
_comp_file = Path(__file__).parent / "data" / "companion_blacklist.txt"
if _comp_file.exists():
for _line in _comp_file.read_text(encoding="utf-8").splitlines():
_w = _line.strip()
if _w and not _w.startswith("#"):
_COMPANION_BLACKLIST_SET.add(_w.upper())
log.info("Companion blacklist chargée : %d entrées", len(_COMPANION_BLACKLIST_SET))
_COMPANION_BLACKLIST_SET: set = _load_txt_set(
Path(__file__).parent / "data" / "companion_blacklist.txt",
transform=str.upper,
label="Companion blacklist",
)
if not _COMPANION_BLACKLIST_SET:
_COMPANION_BLACKLIST_SET = set(_COMPANION_BLACKLIST_FALLBACK)
_WHITELIST_FUNCTION_WORDS = {