diff --git a/launcher.py b/launcher.py index 85dbaaa..90f8df3 100644 --- a/launcher.py +++ b/launcher.py @@ -105,48 +105,104 @@ def check_models_ready(): def launch_gui(): - """Launch the main GUI with a splash screen during the slow core import. + """Launch the main GUI with a splash screen + progression des chargements. - Le chargement des gazetteers (INSEE 200k+ noms, FINESS 100k+ établissements, - BDPM 7k+ médicaments) prend 10–30 s. Sans feedback visuel, l'utilisateur - croit que l'application est plantée. Deux splashes collaborent : - - Splash natif PyInstaller (image) : visible AVANT le démarrage Python, - pendant la décompression --onefile dans %TEMP%. - - Splash tkinter ci-dessous : progressbar + messages pendant l'import - des modèles et la construction de la GUI. + Pendant les 15-30 s d'import du core (gazetteers INSEE/FINESS/BDPM, + modèles ONNX), on intercepte les log.info() via un handler dédié pour + les afficher DANS LE SPLASH au fur et à mesure. L'utilisateur voit + défiler les étapes de chargement — effet pro. + + Deux splashes collaborent : + - Splash natif PyInstaller (image visible AVANT le démarrage Python) : + mis à jour via pyi_splash.update_text(). + - Splash tkinter ci-dessous : zone "Étapes" qui affiche la ligne courante + et la dernière étape terminée. """ log.info("Launching GUI...") - _splash_update("Chargement de l'interface…") - splash = tk.Tk() splash.title("Anonymisation") - splash.geometry("440x200") + splash.geometry("480x240") 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)) + foreground="#666666").pack(pady=(0, 8)) - status_var = tk.StringVar(value="Chargement des dictionnaires médicaux…") - ttk.Label(frame, textvariable=status_var, foreground="#1a568e").pack(pady=(0, 8)) + status_var = tk.StringVar(value="Démarrage…") + ttk.Label(frame, textvariable=status_var, foreground="#E91E63", + font=("", 10, "bold")).pack(pady=(4, 4)) - pb = ttk.Progressbar(frame, mode="indeterminate", length=380) + # Zone où défile le détail des étapes (dernier log.info interceptée) + detail_var = tk.StringVar(value="") + ttk.Label(frame, textvariable=detail_var, foreground="#666666", + font=("", 9), wraplength=440).pack(pady=(0, 8)) + + pb = ttk.Progressbar(frame, mode="indeterminate", length=420) pb.pack(pady=4) pb.start(10) - ttk.Label(frame, text="Première phase : ~15–30 s", + ttk.Label(frame, text="Merci de patienter — 15 à 30 secondes", foreground="#999999", font=("", 8)).pack(pady=(6, 0)) result = {"done": False, "error": None} + # --- Handler de logs qui alimente le splash --- + # Traduit les messages techniques vers des libellés "prod" plus lisibles. + _LOG_TRANSLATIONS = [ + ("Gazetteers INSEE prénoms", "Chargement des prénoms français (INSEE)…"), + ("Gazetteers INSEE communes", "Chargement des communes françaises (INSEE)…"), + ("Gazetteers INSEE noms de famille", "Chargement des noms de famille (INSEE)…"), + ("Villes blacklist", "Chargement de la blacklist des villes…"), + ("Gazetteer FINESS numéros", "Chargement des numéros FINESS…"), + ("Gazetteer FINESS villes", "Chargement des villes FINESS…"), + ("Gazetteer FINESS téléphones", "Chargement des téléphones FINESS…"), + ("Gazetteer FINESS Aho-Corasick", "Indexation des établissements de santé…"), + ("Gazetteer FINESS adresses", "Chargement des adresses FINESS…"), + ("Gazetteer VILLE Aho-Corasick", "Indexation des villes…"), + ("Whitelist termes médicaux", "Chargement du lexique médical…"), + ("Whitelist médicaments", "Chargement de la base médicamenteuse (BDPM)…"), + ("Stop-words manuels", "Chargement des stop-words…"), + ("BDPM stop-words", "Chargement des médicaments BDPM…"), + ("DPI labels blacklist", "Chargement des libellés DPI…"), + ("Companion blacklist", "Chargement du vocabulaire clinique…"), + ("Whitelist phrases", "Chargement des phrases protégées…"), + ("FINESS mono-mots", "Chargement des sigles d'établissement…"), + ("Core imported OK", "Moteur d'anonymisation prêt…"), + ("GUI module imported OK", "Interface prête — finalisation…"), + ] + + def _translate(msg: str) -> str: + for key, human in _LOG_TRANSLATIONS: + if key in msg: + return human + return msg + + class _SplashHandler(logging.Handler): + def emit(self, record): + try: + msg = record.getMessage() + human = _translate(msg) + # Update splash tkinter (thread-safe via after) + splash.after(0, lambda m=human: detail_var.set(m[:120])) + # Update splash natif PyInstaller (si encore actif) + _splash_update(human) + except Exception: + pass + + _splash_handler = _SplashHandler() + _splash_handler.setLevel(logging.INFO) + logging.getLogger().addHandler(_splash_handler) + def _do_import(): try: + status_var.set("Chargement des dictionnaires médicaux…") import anonymizer_core_refactored_onnx # noqa log.info("Core imported OK") result["step"] = "gui" + status_var.set("Chargement de l'interface…") import Pseudonymisation_Gui_V5 # noqa log.info("GUI module imported OK") except Exception as e: @@ -163,11 +219,13 @@ def launch_gui(): _splash_close() 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() + # Retirer le handler — la GUI principale utilise ses propres logs + try: + logging.getLogger().removeHandler(_splash_handler) + except Exception: + pass try: splash.destroy() except Exception: