From 0a377bc001e8e63fbcb4fe51e1344d12506a2431 Mon Sep 17 00:00:00 2001 From: Domi31tls Date: Wed, 15 Apr 2026 15:28:45 +0200 Subject: [PATCH] =?UTF-8?q?feat(splash):=20splash=20natif=20PyInstaller=20?= =?UTF-8?q?=E2=80=94=20couvre=20la=20d=C3=A9compression=20onefile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'exe --onefile décompresse ~720 Mo dans %TEMP% au lancement. Sur Windows, cela prend 15-30 s AVANT que Python ne démarre. Pendant ce temps : - Aucune fenêtre visible (même le splash tkinter existant n'était pas encore exécuté, car il faut d'abord l'import de Python). - L'utilisateur clique parfois plusieurs fois, croit que l'app est plantée. Solution : Splash natif PyInstaller (Splash() dans le .spec). L'image est affichée PAR LE BOOTLOADER de l'exe, AVANT même le démarrage Python. Le texte sous l'image est actualisable via pyi_splash.update_text(), puis fermé via pyi_splash.close() une fois le splash tkinter visible. Changements : - assets/splash.png (480x240) : titre + sous-titre + indication de durée - anonymisation_onefile.spec : Splash() + splash/splash.binaries dans EXE() - launcher.py : import pyi_splash (fallback silencieux en mode dev), helpers _splash_update / _splash_close, fermeture du splash natif dès que le splash tkinter est à l'écran (évite superposition). - .gitignore : exception !assets/** pour versionner l'image du splash (règle générale *.png exclut tout le reste). Effet utilisateur attendu : fenêtre visible IMMÉDIATEMENT au double-clic, avec message "Démarrage en cours — merci de patienter…". Suppression du trou noir de 15-30 s. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 +++ anonymisation_onefile.spec | 21 ++++++++++++++++- assets/splash.png | Bin 0 -> 10092 bytes launcher.py | 45 ++++++++++++++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 assets/splash.png 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 0000000000000000000000000000000000000000..f201e625348d09cf5d83d57c33ad9d2f88b3b139 GIT binary patch literal 10092 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 Z9^7VmLwsKZe9K1hTwPDCRK@z;e*swKSt0-c literal 0 HcmV?d00001 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: