diff --git a/.gitignore b/.gitignore index 7cc8b7e..bd18cc4 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ models/ *.jpg *.jpeg *.gif +# Exception : assets embarqués dans l'exe (splash, icônes…) doivent être versionnés +!assets/** +!assets *.mp3 *.wav *.mp4 diff --git a/anonymisation_onefile.spec b/anonymisation_onefile.spec index 40d1d8e..1584c1c 100644 --- a/anonymisation_onefile.spec +++ b/anonymisation_onefile.spec @@ -45,8 +45,27 @@ a = Analysis( noarchive=False, ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +# Splash natif PyInstaller : image affichée AU LANCEMENT DE L'EXE, +# avant même que Python démarre. Couvre les ~15-30 s de décompression +# du bundle --onefile dans %TEMP% qui laissaient l'écran vide auparavant. +# Le launcher ferme le splash via pyi_splash.close() une fois la GUI prête. +splash = Splash( + os.path.join(app_dir, 'assets', 'splash.png'), + binaries=a.binaries, + datas=a.datas, + text_pos=(10, 215), + text_size=10, + text_color='white', + minify_script=True, + always_on_top=False, +) + exe = EXE( - pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], + pyz, a.scripts, + splash, # image affichée immédiatement + splash.binaries, # bootloader splash + a.binaries, a.zipfiles, a.datas, [], name='Anonymisation', debug=False, strip=False, diff --git a/assets/splash.png b/assets/splash.png new file mode 100644 index 0000000..f201e62 Binary files /dev/null and b/assets/splash.png differ diff --git a/launcher.py b/launcher.py index 9970263..85dbaaa 100644 --- a/launcher.py +++ b/launcher.py @@ -9,6 +9,35 @@ from pathlib import Path import threading import logging +# pyi_splash : module injecté par PyInstaller quand --splash est utilisé. +# Permet d'actualiser / fermer le splash natif affiché au démarrage de l'exe +# pendant la décompression --onefile (~15-30 s sur Windows). En mode dev +# (pas frozen), le module n'existe pas → fallback silencieux. +try: + import pyi_splash # type: ignore + _HAS_PYI_SPLASH = True +except Exception: + pyi_splash = None + _HAS_PYI_SPLASH = False + + +def _splash_update(text: str) -> None: + """Met à jour le texte affiché sous le splash natif PyInstaller (si actif).""" + if _HAS_PYI_SPLASH: + try: + pyi_splash.update_text(text) + except Exception: + pass + + +def _splash_close() -> None: + """Ferme le splash natif PyInstaller (si actif).""" + if _HAS_PYI_SPLASH: + try: + pyi_splash.close() + except Exception: + pass + # --------------------------------------------------------------------------- # Single-instance guard (lock file in user's temp directory) # --------------------------------------------------------------------------- @@ -80,10 +109,16 @@ def launch_gui(): 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. Le splash permet d'indiquer l'avancée. + 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. """ log.info("Launching GUI...") + _splash_update("Chargement de l'interface…") + splash = tk.Tk() splash.title("Anonymisation") splash.geometry("440x200") @@ -122,6 +157,11 @@ def launch_gui(): threading.Thread(target=_do_import, daemon=True).start() + # Fermer le splash natif PyInstaller dès que le splash tkinter est à l'écran + # (sinon les deux se superposent pendant tout l'import). + splash.update() # force le rendu initial + _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"]: @@ -358,8 +398,11 @@ def main(): try: if check_models_ready(): + _splash_update("Modèles déjà installés — chargement…") launch_gui() else: + _splash_update("Premier lancement — configuration initiale") + _splash_close() # laisse place à la SetupWindow qui a sa propre UI setup = SetupWindow() setup.run() except Exception as e: