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:
@@ -11,6 +11,18 @@ datas = [
|
|||||||
(os.path.join(app_dir, 'detectors'), 'detectors'),
|
(os.path.join(app_dir, 'detectors'), 'detectors'),
|
||||||
(os.path.join(app_dir, 'scripts'), 'scripts'),
|
(os.path.join(app_dir, 'scripts'), 'scripts'),
|
||||||
]
|
]
|
||||||
|
# Fichiers directs dans data/ — IMPÉRATIF pour fonctionnement correct du core.
|
||||||
|
# Sans eux : stop-words/villes/DPI labels/companion blacklist sont des sets vides,
|
||||||
|
# ce qui dégrade la qualité d'anonymisation et peut masquer/laisser passer des faux-positifs.
|
||||||
|
for data_file in [
|
||||||
|
'stopwords_manuels.txt',
|
||||||
|
'villes_blacklist.txt',
|
||||||
|
'dpi_labels_blacklist.txt',
|
||||||
|
'companion_blacklist.txt',
|
||||||
|
]:
|
||||||
|
src = os.path.join(app_dir, 'data', data_file)
|
||||||
|
if os.path.exists(src):
|
||||||
|
datas.append((src, 'data'))
|
||||||
for pyfile in ['anonymizer_core_refactored_onnx.py', 'eds_pseudo_manager.py',
|
for pyfile in ['anonymizer_core_refactored_onnx.py', 'eds_pseudo_manager.py',
|
||||||
'gliner_manager.py', 'camembert_ner_manager.py',
|
'gliner_manager.py', 'camembert_ner_manager.py',
|
||||||
'Pseudonymisation_Gui_V5.py']:
|
'Pseudonymisation_Gui_V5.py']:
|
||||||
|
|||||||
@@ -251,11 +251,16 @@ _VILLE_BLACKLIST = {
|
|||||||
# Enrichissement depuis fichier externe (modifiable sans toucher au code)
|
# Enrichissement depuis fichier externe (modifiable sans toucher au code)
|
||||||
_villes_bl_file = Path(__file__).parent / "data" / "villes_blacklist.txt"
|
_villes_bl_file = Path(__file__).parent / "data" / "villes_blacklist.txt"
|
||||||
if _villes_bl_file.exists():
|
if _villes_bl_file.exists():
|
||||||
for _line in _villes_bl_file.read_text(encoding="utf-8").splitlines():
|
try:
|
||||||
_w = _line.strip()
|
for _line in _villes_bl_file.read_text(encoding="utf-8").splitlines():
|
||||||
if _w and not _w.startswith("#"):
|
_w = _line.strip()
|
||||||
_VILLE_BLACKLIST.add(_w)
|
if _w and not _w.startswith("#"):
|
||||||
log.info("Villes blacklist chargées : %d entrées", len(_VILLE_BLACKLIST))
|
_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:
|
try:
|
||||||
import ahocorasick as _ahocorasick
|
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)
|
# Enrichissement depuis fichier externe (modifiable sans toucher au code)
|
||||||
_stopwords_file = Path(__file__).parent / "data" / "stopwords_manuels.txt"
|
_stopwords_file = Path(__file__).parent / "data" / "stopwords_manuels.txt"
|
||||||
if _stopwords_file.exists():
|
if _stopwords_file.exists():
|
||||||
_sw_count = 0
|
try:
|
||||||
for _line in _stopwords_file.read_text(encoding="utf-8").splitlines():
|
_sw_count = 0
|
||||||
_w = _line.strip()
|
for _line in _stopwords_file.read_text(encoding="utf-8").splitlines():
|
||||||
if _w and not _w.startswith("#"):
|
_w = _line.strip()
|
||||||
_MEDICAL_STOP_WORDS_SET.add(_w)
|
if _w and not _w.startswith("#"):
|
||||||
_sw_count += 1
|
_MEDICAL_STOP_WORDS_SET.add(_w)
|
||||||
log.info("Stop-words manuels chargés : %d mots depuis %s", _sw_count, _stopwords_file.name)
|
_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
|
# Enrichissement BDPM : ~7300 noms commerciaux + DCI/substances actives
|
||||||
_bdpm_path = Path(__file__).parent / "data" / "bdpm" / "medicaments_stopwords.txt"
|
_bdpm_path = Path(__file__).parent / "data" / "bdpm" / "medicaments_stopwords.txt"
|
||||||
if _bdpm_path.exists():
|
if _bdpm_path.exists():
|
||||||
_bdpm_count = 0
|
try:
|
||||||
for _line in _bdpm_path.read_text(encoding="utf-8").splitlines():
|
_bdpm_count = 0
|
||||||
_w = _line.strip()
|
for _line in _bdpm_path.read_text(encoding="utf-8").splitlines():
|
||||||
if _w and not _w.startswith("#"):
|
_w = _line.strip()
|
||||||
_MEDICAL_STOP_WORDS_SET.add(_w)
|
if _w and not _w.startswith("#"):
|
||||||
_bdpm_count += 1
|
_MEDICAL_STOP_WORDS_SET.add(_w)
|
||||||
log.info("BDPM stop-words chargés : %d mots", _bdpm_count)
|
_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 = (
|
_MEDICAL_STOP_WORDS = (
|
||||||
r"(?:" + "|".join(re.escape(w) for w in _MEDICAL_STOP_WORDS_SET) + r")"
|
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_TOKENS: set = set()
|
||||||
_WHITELIST_NEVER_MASK_PHRASES: 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...)
|
# Labels DPI structurels à ne JAMAIS masquer comme noms (Date, Note, Heure...)
|
||||||
# Stocké en LOWERCASE — la comparaison est case-insensitive.
|
# Stocké en LOWERCASE — la comparaison est case-insensitive.
|
||||||
# Chargé depuis data/dpi_labels_blacklist.txt + cfg["additional_dpi_labels"].
|
# Chargé depuis data/dpi_labels_blacklist.txt + cfg["additional_dpi_labels"].
|
||||||
_DPI_LABELS_SET: set = set()
|
_DPI_LABELS_SET: set = _load_txt_set(
|
||||||
_dpi_file = Path(__file__).parent / "data" / "dpi_labels_blacklist.txt"
|
Path(__file__).parent / "data" / "dpi_labels_blacklist.txt",
|
||||||
if _dpi_file.exists():
|
transform=str.lower,
|
||||||
for _line in _dpi_file.read_text(encoding="utf-8").splitlines():
|
label="DPI labels blacklist",
|
||||||
_w = _line.strip()
|
)
|
||||||
if _w and not _w.startswith("#"):
|
if not _DPI_LABELS_SET:
|
||||||
_DPI_LABELS_SET.add(_w.lower())
|
_DPI_LABELS_SET = set(_DPI_LABELS_FALLBACK)
|
||||||
log.info("DPI labels blacklist chargés : %d entrées", len(_DPI_LABELS_SET))
|
|
||||||
|
|
||||||
# Companion blacklist : termes EN MAJUSCULES qui ne sont JAMAIS des noms
|
# Companion blacklist : termes EN MAJUSCULES qui ne sont JAMAIS des noms
|
||||||
# (spécialités, labos pharma, mots courants ambigus).
|
# (spécialités, labos pharma, mots courants ambigus).
|
||||||
# Stocké en UPPERCASE — la comparaison est faite contre des candidats déjà uppercase.
|
# Stocké en UPPERCASE — la comparaison est faite contre des candidats déjà uppercase.
|
||||||
# Chargé depuis data/companion_blacklist.txt + cfg["additional_companion_blacklist"].
|
# Chargé depuis data/companion_blacklist.txt + cfg["additional_companion_blacklist"].
|
||||||
_COMPANION_BLACKLIST_SET: set = set()
|
_COMPANION_BLACKLIST_SET: set = _load_txt_set(
|
||||||
_comp_file = Path(__file__).parent / "data" / "companion_blacklist.txt"
|
Path(__file__).parent / "data" / "companion_blacklist.txt",
|
||||||
if _comp_file.exists():
|
transform=str.upper,
|
||||||
for _line in _comp_file.read_text(encoding="utf-8").splitlines():
|
label="Companion blacklist",
|
||||||
_w = _line.strip()
|
)
|
||||||
if _w and not _w.startswith("#"):
|
if not _COMPANION_BLACKLIST_SET:
|
||||||
_COMPANION_BLACKLIST_SET.add(_w.upper())
|
_COMPANION_BLACKLIST_SET = set(_COMPANION_BLACKLIST_FALLBACK)
|
||||||
log.info("Companion blacklist chargée : %d entrées", len(_COMPANION_BLACKLIST_SET))
|
|
||||||
|
|
||||||
|
|
||||||
_WHITELIST_FUNCTION_WORDS = {
|
_WHITELIST_FUNCTION_WORDS = {
|
||||||
|
|||||||
250
launcher.py
250
launcher.py
@@ -76,112 +76,264 @@ def check_models_ready():
|
|||||||
|
|
||||||
|
|
||||||
def launch_gui():
|
def launch_gui():
|
||||||
"""Launch the main GUI."""
|
"""Launch the main GUI with a splash screen during the slow core import.
|
||||||
log.info("Launching GUI...")
|
|
||||||
try:
|
|
||||||
import anonymizer_core_refactored_onnx # noqa — pre-import the core
|
|
||||||
log.info("Core imported OK")
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f"Core import error: {e}\n{traceback.format_exc()}")
|
|
||||||
|
|
||||||
try:
|
Le chargement des gazetteers (INSEE 200k+ noms, FINESS 100k+ établissements,
|
||||||
import Pseudonymisation_Gui_V5 # noqa
|
BDPM 7k+ médicaments) prend 10–30 s. Sans feedback visuel, l'utilisateur
|
||||||
log.info("GUI imported OK, starting mainloop...")
|
croit que l'application est plantée. Le splash permet d'indiquer l'avancée.
|
||||||
root = tk.Tk()
|
"""
|
||||||
Pseudonymisation_Gui_V5.App(root)
|
log.info("Launching GUI...")
|
||||||
root.mainloop()
|
|
||||||
except Exception as e:
|
splash = tk.Tk()
|
||||||
log.error(f"GUI error: {e}\n{traceback.format_exc()}")
|
splash.title("Anonymisation")
|
||||||
|
splash.geometry("440x200")
|
||||||
|
splash.resizable(False, False)
|
||||||
|
|
||||||
|
frame = ttk.Frame(splash, padding=20)
|
||||||
|
frame.pack(fill="both", expand=True)
|
||||||
|
ttk.Label(frame, text="Anonymisation", font=("", 14, "bold")).pack(pady=(0, 4))
|
||||||
|
ttk.Label(frame, text="Pseudonymisation de documents médicaux",
|
||||||
|
foreground="#666666").pack(pady=(0, 12))
|
||||||
|
|
||||||
|
status_var = tk.StringVar(value="Chargement des dictionnaires médicaux…")
|
||||||
|
ttk.Label(frame, textvariable=status_var, foreground="#1a568e").pack(pady=(0, 8))
|
||||||
|
|
||||||
|
pb = ttk.Progressbar(frame, mode="indeterminate", length=380)
|
||||||
|
pb.pack(pady=4)
|
||||||
|
pb.start(10)
|
||||||
|
|
||||||
|
ttk.Label(frame, text="Première phase : ~15–30 s",
|
||||||
|
foreground="#999999", font=("", 8)).pack(pady=(6, 0))
|
||||||
|
|
||||||
|
result = {"done": False, "error": None}
|
||||||
|
|
||||||
|
def _do_import():
|
||||||
try:
|
try:
|
||||||
messagebox.showerror("Erreur", f"Erreur au lancement de l'interface:\n\n{e}\n\nVoir {LOG_FILE}")
|
import anonymizer_core_refactored_onnx # noqa
|
||||||
except:
|
log.info("Core imported OK")
|
||||||
pass
|
result["step"] = "gui"
|
||||||
|
import Pseudonymisation_Gui_V5 # noqa
|
||||||
|
log.info("GUI module imported OK")
|
||||||
|
except Exception as e:
|
||||||
|
result["error"] = f"{e}\n{traceback.format_exc()}"
|
||||||
|
log.error(f"Import error: {result['error']}")
|
||||||
|
finally:
|
||||||
|
result["done"] = True
|
||||||
|
|
||||||
|
threading.Thread(target=_do_import, daemon=True).start()
|
||||||
|
|
||||||
|
def _poll():
|
||||||
|
# Met à jour le message selon l'étape atteinte par le thread d'import
|
||||||
|
if result.get("step") == "gui" and not result["done"]:
|
||||||
|
status_var.set("Chargement de l'interface…")
|
||||||
|
if result["done"]:
|
||||||
|
pb.stop()
|
||||||
|
try:
|
||||||
|
splash.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if result["error"]:
|
||||||
|
try:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Erreur",
|
||||||
|
f"Erreur au lancement :\n\n{result['error'].splitlines()[0]}\n\n"
|
||||||
|
f"Voir {LOG_FILE} pour les détails.",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
# Lancement de la GUI principale
|
||||||
|
try:
|
||||||
|
import Pseudonymisation_Gui_V5
|
||||||
|
log.info("Starting mainloop…")
|
||||||
|
root = tk.Tk()
|
||||||
|
Pseudonymisation_Gui_V5.App(root)
|
||||||
|
root.mainloop()
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"GUI error: {e}\n{traceback.format_exc()}")
|
||||||
|
try:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Erreur",
|
||||||
|
f"Erreur de l'interface :\n\n{e}\n\nVoir {LOG_FILE}",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
splash.after(200, _poll)
|
||||||
|
|
||||||
|
splash.after(200, _poll)
|
||||||
|
splash.mainloop()
|
||||||
|
|
||||||
|
|
||||||
class SetupWindow:
|
class SetupWindow:
|
||||||
"""Setup window for first launch (model download)."""
|
"""Setup window for first launch — auto-démarre le téléchargement des modèles.
|
||||||
|
|
||||||
|
Affiche un suivi détaillé par modèle (EDS-Pseudo, GLiNER, CamemBERT-bio) avec
|
||||||
|
indicateurs visuels (⏳ en cours, ✓ succès, ✗ échec). Permet de relancer en
|
||||||
|
cas d'erreur. Lancement auto de la GUI une fois tous les modèles prêts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Liste ordonnée des étapes de chargement. Chaque entrée :
|
||||||
|
# (clé interne, libellé, taille approx, fonction de chargement)
|
||||||
|
STEPS = [
|
||||||
|
("eds_pseudo", "EDS-Pseudo (CamemBERT clinique)", "~450 Mo"),
|
||||||
|
("gliner", "GLiNER (détection PII zero-shot)", "~300 Mo"),
|
||||||
|
("camembert_onnx", "CamemBERT-bio ONNX (embarqué)", "local"),
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
self.root.title("Anonymisation — Premier lancement")
|
self.root.title("Anonymisation — Configuration initiale")
|
||||||
self.root.geometry("520x320")
|
self.root.geometry("620x450")
|
||||||
self.root.resizable(False, False)
|
self.root.resizable(False, False)
|
||||||
|
|
||||||
frame = ttk.Frame(self.root, padding=20)
|
frame = ttk.Frame(self.root, padding=20)
|
||||||
frame.pack(fill="both", expand=True)
|
frame.pack(fill="both", expand=True)
|
||||||
|
|
||||||
ttk.Label(frame, text="Configuration initiale", font=("", 14, "bold")).pack(pady=(0, 10))
|
ttk.Label(frame, text="Préparation des modèles d'intelligence artificielle",
|
||||||
|
font=("", 13, "bold")).pack(pady=(0, 4))
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
frame,
|
frame,
|
||||||
text="Les modeles NER vont etre telecharges depuis HuggingFace.\nCela peut prendre quelques minutes selon la connexion.",
|
text=("Au premier lancement, les modèles de détection doivent être téléchargés\n"
|
||||||
justify="center",
|
"depuis HuggingFace. Cette opération est unique — durée 3 à 10 minutes\n"
|
||||||
).pack(pady=(0, 15))
|
"selon votre connexion internet. Merci de patienter."),
|
||||||
|
justify="center", foreground="#555555",
|
||||||
|
).pack(pady=(0, 12))
|
||||||
|
|
||||||
self.progress = ttk.Progressbar(frame, mode="indeterminate", length=420)
|
# Barre de progression globale
|
||||||
self.progress.pack(pady=10)
|
self.progress = ttk.Progressbar(frame, mode="determinate",
|
||||||
|
length=560, maximum=len(self.STEPS))
|
||||||
|
self.progress.pack(pady=(0, 4))
|
||||||
|
|
||||||
self.status_var = tk.StringVar(value="En attente...")
|
self.status_var = tk.StringVar(value="Démarrage…")
|
||||||
ttk.Label(frame, textvariable=self.status_var).pack(pady=5)
|
ttk.Label(frame, textvariable=self.status_var, foreground="#1a568e").pack(pady=(0, 12))
|
||||||
|
|
||||||
self.btn = ttk.Button(frame, text="Demarrer le telechargement", command=self.start_download)
|
# Zone détail par modèle
|
||||||
self.btn.pack(pady=15)
|
detail_frame = ttk.LabelFrame(frame, text=" Modèles ", padding=10)
|
||||||
|
detail_frame.pack(fill="x", pady=(0, 12))
|
||||||
|
|
||||||
|
self.step_labels = {}
|
||||||
|
for key, title, size in self.STEPS:
|
||||||
|
row = ttk.Frame(detail_frame)
|
||||||
|
row.pack(fill="x", pady=3)
|
||||||
|
icon = ttk.Label(row, text="○", width=3, font=("", 12))
|
||||||
|
icon.pack(side="left")
|
||||||
|
ttk.Label(row, text=title).pack(side="left")
|
||||||
|
ttk.Label(row, text=f" ({size})", foreground="#999999",
|
||||||
|
font=("", 8)).pack(side="left")
|
||||||
|
self.step_labels[key] = icon
|
||||||
|
|
||||||
|
# Bouton relance (caché au début)
|
||||||
|
self.btn = ttk.Button(frame, text="Relancer", command=self.start_download)
|
||||||
|
self.btn.pack(pady=6)
|
||||||
|
self.btn.pack_forget()
|
||||||
|
|
||||||
|
# Bouton ignorer/continuer (affiché si échec partiel)
|
||||||
|
self.btn_skip = ttk.Button(
|
||||||
|
frame, text="Continuer malgré tout",
|
||||||
|
command=self._finish,
|
||||||
|
)
|
||||||
|
self.btn_skip.pack(pady=(0, 4))
|
||||||
|
self.btn_skip.pack_forget()
|
||||||
|
|
||||||
|
# Auto-démarrage du téléchargement (pas besoin de cliquer)
|
||||||
|
self.root.after(500, self.start_download)
|
||||||
|
|
||||||
def start_download(self):
|
def start_download(self):
|
||||||
self.btn.configure(state="disabled")
|
self.btn.pack_forget()
|
||||||
self.progress.start(10)
|
self.btn_skip.pack_forget()
|
||||||
|
self.progress["value"] = 0
|
||||||
|
self.status_var.set("Démarrage du téléchargement…")
|
||||||
|
for icon in self.step_labels.values():
|
||||||
|
icon.configure(text="○", foreground="#999999")
|
||||||
threading.Thread(target=self._download_thread, daemon=True).start()
|
threading.Thread(target=self._download_thread, daemon=True).start()
|
||||||
|
|
||||||
|
def _set_step(self, key, state):
|
||||||
|
"""state : 'pending' | 'running' | 'ok' | 'fail'"""
|
||||||
|
mapping = {
|
||||||
|
"pending": ("○", "#999999"),
|
||||||
|
"running": ("⏳", "#f57c00"),
|
||||||
|
"ok": ("✓", "#2e7d32"),
|
||||||
|
"fail": ("✗", "#c62828"),
|
||||||
|
}
|
||||||
|
char, color = mapping.get(state, ("○", "#999999"))
|
||||||
|
icon = self.step_labels.get(key)
|
||||||
|
if icon is not None:
|
||||||
|
self.root.after(0, lambda: icon.configure(text=char, foreground=color))
|
||||||
|
|
||||||
def _download_thread(self):
|
def _download_thread(self):
|
||||||
|
failures = []
|
||||||
try:
|
try:
|
||||||
# 1. EDS-Pseudo
|
# 1. EDS-Pseudo
|
||||||
self._update("Chargement EDS-Pseudo (telechargement HuggingFace)...")
|
self._update("Téléchargement d'EDS-Pseudo… (modèle CamemBERT clinique)")
|
||||||
|
self._set_step("eds_pseudo", "running")
|
||||||
log.info("Downloading EDS-Pseudo...")
|
log.info("Downloading EDS-Pseudo...")
|
||||||
try:
|
try:
|
||||||
from eds_pseudo_manager import EdsPseudoManager
|
from eds_pseudo_manager import EdsPseudoManager
|
||||||
mgr = EdsPseudoManager()
|
mgr = EdsPseudoManager()
|
||||||
mgr.load()
|
mgr.load()
|
||||||
self._update("EDS-Pseudo OK")
|
self._set_step("eds_pseudo", "ok")
|
||||||
log.info("EDS-Pseudo OK")
|
log.info("EDS-Pseudo OK")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._update(f"EDS-Pseudo: {e}")
|
self._set_step("eds_pseudo", "fail")
|
||||||
|
failures.append(("EDS-Pseudo", str(e)))
|
||||||
log.warning(f"EDS-Pseudo failed: {e}")
|
log.warning(f"EDS-Pseudo failed: {e}")
|
||||||
|
self._advance()
|
||||||
|
|
||||||
# 2. GLiNER
|
# 2. GLiNER
|
||||||
self._update("Chargement GLiNER (telechargement HuggingFace)...")
|
self._update("Téléchargement de GLiNER… (détection zero-shot)")
|
||||||
|
self._set_step("gliner", "running")
|
||||||
log.info("Downloading GLiNER...")
|
log.info("Downloading GLiNER...")
|
||||||
try:
|
try:
|
||||||
from gliner_manager import GlinerManager
|
from gliner_manager import GlinerManager
|
||||||
mgr = GlinerManager()
|
mgr = GlinerManager()
|
||||||
mgr.load()
|
mgr.load()
|
||||||
self._update("GLiNER OK")
|
self._set_step("gliner", "ok")
|
||||||
log.info("GLiNER OK")
|
log.info("GLiNER OK")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._update(f"GLiNER: {e}")
|
self._set_step("gliner", "fail")
|
||||||
|
failures.append(("GLiNER", str(e)))
|
||||||
log.warning(f"GLiNER failed: {e}")
|
log.warning(f"GLiNER failed: {e}")
|
||||||
|
self._advance()
|
||||||
|
|
||||||
# 3. CamemBERT-bio ONNX
|
# 3. CamemBERT-bio ONNX
|
||||||
self._update("Verification CamemBERT-bio ONNX...")
|
self._update("Vérification CamemBERT-bio ONNX (modèle embarqué)…")
|
||||||
|
self._set_step("camembert_onnx", "running")
|
||||||
if check_models_ready():
|
if check_models_ready():
|
||||||
self._update("CamemBERT-bio ONNX OK")
|
self._set_step("camembert_onnx", "ok")
|
||||||
else:
|
else:
|
||||||
self._update("CamemBERT-bio ONNX manquant")
|
self._set_step("camembert_onnx", "fail")
|
||||||
|
failures.append(("CamemBERT-bio ONNX", "fichier ONNX introuvable dans le bundle"))
|
||||||
log.error("CamemBERT-bio ONNX not found")
|
log.error("CamemBERT-bio ONNX not found")
|
||||||
|
self._advance()
|
||||||
|
|
||||||
self._update("Configuration terminee ! Lancement de l'interface...")
|
if failures:
|
||||||
log.info("Setup complete, launching GUI in 2s")
|
lines = "\n".join(f" • {name} : {err[:60]}" for name, err in failures)
|
||||||
self.root.after(2000, self._finish)
|
self._update(f"Certains modèles ont échoué ({len(failures)}/{len(self.STEPS)}).")
|
||||||
|
log.warning(f"Setup partial failure: {len(failures)} model(s) failed\n{lines}")
|
||||||
|
self.root.after(0, lambda: self.btn.pack(pady=6))
|
||||||
|
self.root.after(0, lambda: self.btn_skip.pack(pady=(0, 4)))
|
||||||
|
else:
|
||||||
|
self._update("Tous les modèles sont prêts. Lancement de l'interface…")
|
||||||
|
log.info("Setup complete, launching GUI in 1.5s")
|
||||||
|
self.root.after(1500, self._finish)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Setup error: {e}\n{traceback.format_exc()}")
|
log.error(f"Setup error: {e}\n{traceback.format_exc()}")
|
||||||
self._update(f"Erreur: {e}")
|
self._update(f"Erreur inattendue : {e}")
|
||||||
self.root.after(0, lambda: self.btn.configure(state="normal"))
|
self.root.after(0, lambda: self.btn.pack(pady=6))
|
||||||
|
|
||||||
|
def _advance(self):
|
||||||
|
self.root.after(0, lambda: self.progress.step(1))
|
||||||
|
|
||||||
def _update(self, msg):
|
def _update(self, msg):
|
||||||
self.root.after(0, lambda: self.status_var.set(msg))
|
self.root.after(0, lambda: self.status_var.set(msg))
|
||||||
|
|
||||||
def _finish(self):
|
def _finish(self):
|
||||||
self.progress.stop()
|
try:
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
launch_gui()
|
launch_gui()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user