feat(splash): splash natif PyInstaller — couvre la décompression onefile

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 15:28:45 +02:00
parent e2e2a7c8e3
commit 0a377bc001
4 changed files with 67 additions and 2 deletions

View File

@@ -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 1030 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: