backup: WIP Windows avant repart propre (GUI core installer splash spec)
This commit is contained in:
315
launcher.py
315
launcher.py
@@ -8,6 +8,8 @@ from tkinter import ttk, messagebox
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import logging
|
||||
import contextlib
|
||||
import time
|
||||
|
||||
# pyi_splash : module injecté par PyInstaller quand --splash est utilisé.
|
||||
# Permet d'actualiser / fermer le splash natif affiché au démarrage de l'exe
|
||||
@@ -38,6 +40,216 @@ def _splash_close() -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class BrandedSplash:
|
||||
"""Splash applicatif avec le visuel existant + progression détaillée.
|
||||
|
||||
PyInstaller affiche d'abord le splash natif pendant l'extraction du onefile.
|
||||
Dès que Python est démarré, cette fenêtre prend le relais pour montrer des
|
||||
étapes lisibles et un petit journal de chargement.
|
||||
"""
|
||||
|
||||
def __init__(self, total_steps: int = 6):
|
||||
self.total_steps = max(total_steps, 1)
|
||||
self.current_step = 0
|
||||
self.enabled = False
|
||||
self.root = None
|
||||
self.status_var = None
|
||||
self.progress = None
|
||||
self.log_box = None
|
||||
self._image = None
|
||||
self._lines = []
|
||||
|
||||
try:
|
||||
self.root = tk.Tk()
|
||||
self.root.withdraw()
|
||||
self.root.title("aivanonym")
|
||||
self.root.resizable(False, False)
|
||||
self.root.overrideredirect(True)
|
||||
self.root.configure(bg="white")
|
||||
|
||||
container = tk.Frame(
|
||||
self.root,
|
||||
bg="white",
|
||||
highlightthickness=1,
|
||||
highlightbackground="#d8d8d8",
|
||||
)
|
||||
container.pack(fill="both", expand=True)
|
||||
|
||||
splash_path = APP_DIR / "assets" / "splash.png"
|
||||
if splash_path.exists():
|
||||
self._image = tk.PhotoImage(file=str(splash_path))
|
||||
tk.Label(container, image=self._image, bg="white", bd=0).pack()
|
||||
else:
|
||||
fallback = tk.Frame(container, bg="white", width=500, height=170)
|
||||
fallback.pack_propagate(False)
|
||||
fallback.pack()
|
||||
tk.Frame(fallback, bg="#cc0000", height=4).pack(fill="x")
|
||||
tk.Label(
|
||||
fallback,
|
||||
text="aivanonym",
|
||||
bg="white",
|
||||
fg="#222222",
|
||||
font=("Segoe UI", 28),
|
||||
).pack(expand=True)
|
||||
|
||||
body = tk.Frame(container, bg="white", padx=24, pady=14)
|
||||
body.pack(fill="x")
|
||||
|
||||
self.status_var = tk.StringVar(value="Initialisation...")
|
||||
tk.Label(
|
||||
body,
|
||||
textvariable=self.status_var,
|
||||
bg="white",
|
||||
fg="#222222",
|
||||
font=("Segoe UI", 10, "bold"),
|
||||
anchor="w",
|
||||
).pack(fill="x")
|
||||
|
||||
self.progress = ttk.Progressbar(
|
||||
body,
|
||||
mode="determinate",
|
||||
maximum=self.total_steps,
|
||||
length=452,
|
||||
)
|
||||
self.progress.pack(fill="x", pady=(8, 10))
|
||||
|
||||
tk.Label(
|
||||
body,
|
||||
text="Chargements en cours",
|
||||
bg="white",
|
||||
fg="#666666",
|
||||
font=("Segoe UI", 8),
|
||||
anchor="w",
|
||||
).pack(fill="x")
|
||||
self.log_box = tk.Listbox(
|
||||
body,
|
||||
height=5,
|
||||
activestyle="none",
|
||||
bg="#f7f7f7",
|
||||
fg="#333333",
|
||||
bd=0,
|
||||
highlightthickness=1,
|
||||
highlightbackground="#e7e7e7",
|
||||
font=("Consolas", 8),
|
||||
)
|
||||
self.log_box.pack(fill="x", pady=(4, 0))
|
||||
|
||||
self._center()
|
||||
self.root.deiconify()
|
||||
self.root.lift()
|
||||
self.root.update_idletasks()
|
||||
self.root.update()
|
||||
self.enabled = True
|
||||
|
||||
# Le splash natif PyInstaller n'a qu'une ligne de texte. Une fois
|
||||
# cette fenêtre prête, elle prend le relais sans changer le visuel.
|
||||
_splash_close()
|
||||
except Exception as exc:
|
||||
try:
|
||||
if self.root is not None:
|
||||
self.root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self.root = None
|
||||
log.warning(f"Branded splash unavailable: {exc}")
|
||||
|
||||
def _center(self) -> None:
|
||||
if self.root is None:
|
||||
return
|
||||
self.root.update_idletasks()
|
||||
width = self.root.winfo_reqwidth()
|
||||
height = self.root.winfo_reqheight()
|
||||
screen_width = self.root.winfo_screenwidth()
|
||||
screen_height = self.root.winfo_screenheight()
|
||||
x = max(0, int((screen_width - width) / 2))
|
||||
y = max(0, int((screen_height - height) / 2))
|
||||
self.root.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
def step(self, message: str) -> None:
|
||||
self.current_step = min(self.current_step + 1, self.total_steps)
|
||||
status = f"[{self.current_step}/{self.total_steps}] {message}"
|
||||
self.message(status)
|
||||
if self.progress is not None:
|
||||
self.progress["value"] = self.current_step
|
||||
self._pump()
|
||||
|
||||
def message(self, message: str) -> None:
|
||||
_splash_update(message)
|
||||
if self.enabled and self.status_var is not None:
|
||||
self.status_var.set(message)
|
||||
self._pump()
|
||||
|
||||
def detail(self, message: str) -> None:
|
||||
_splash_update(message)
|
||||
clean = " ".join(str(message).split())
|
||||
if not clean:
|
||||
return
|
||||
if len(clean) > 150:
|
||||
clean = clean[:147] + "..."
|
||||
if self.enabled and self.log_box is not None:
|
||||
self._lines.append(clean)
|
||||
self._lines = self._lines[-7:]
|
||||
self.log_box.delete(0, tk.END)
|
||||
for line in self._lines:
|
||||
self.log_box.insert(tk.END, line)
|
||||
self.log_box.see(tk.END)
|
||||
self._pump()
|
||||
|
||||
def close(self) -> None:
|
||||
_splash_close()
|
||||
if self.root is not None:
|
||||
try:
|
||||
self.root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self.root = None
|
||||
self.enabled = False
|
||||
|
||||
def _pump(self) -> None:
|
||||
if self.root is None:
|
||||
return
|
||||
try:
|
||||
self.root.update_idletasks()
|
||||
self.root.update()
|
||||
except Exception:
|
||||
self.enabled = False
|
||||
|
||||
|
||||
class ModelProgressStream:
|
||||
"""Redirige les sorties type tqdm vers une callback UI."""
|
||||
|
||||
def __init__(self, callback, prefix: str):
|
||||
self.callback = callback
|
||||
self.prefix = prefix
|
||||
self.buffer = ""
|
||||
self.last_line = ""
|
||||
self.last_emit = 0.0
|
||||
|
||||
def write(self, data) -> int:
|
||||
text = str(data)
|
||||
self.buffer += text.replace("\r", "\n")
|
||||
while "\n" in self.buffer:
|
||||
line, self.buffer = self.buffer.split("\n", 1)
|
||||
self._emit(line)
|
||||
return len(text)
|
||||
|
||||
def flush(self) -> None:
|
||||
if self.buffer:
|
||||
self._emit(self.buffer)
|
||||
self.buffer = ""
|
||||
|
||||
def _emit(self, line: str) -> None:
|
||||
clean = " ".join(line.split())
|
||||
if len(clean) < 3:
|
||||
return
|
||||
now = time.monotonic()
|
||||
if clean == self.last_line and now - self.last_emit < 1.0:
|
||||
return
|
||||
self.last_line = clean
|
||||
self.last_emit = now
|
||||
self.callback(f"{self.prefix} : {clean}")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Single-instance guard (lock file in user's temp directory)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -105,23 +317,10 @@ def check_models_ready():
|
||||
|
||||
|
||||
def launch_gui():
|
||||
"""Launch the main GUI — étapes de chargement affichées DANS le splash natif.
|
||||
|
||||
Le splash natif PyInstaller (image avec logo + texte dynamique) reste
|
||||
visible pendant TOUTE la phase de chargement. On intercepte les log.info()
|
||||
du core via un logging.Handler et on pousse chaque étape traduite dans
|
||||
le splash natif via pyi_splash.update_text(). L'utilisateur voit défiler
|
||||
sous le logo :
|
||||
"Chargement des prénoms français (INSEE)…"
|
||||
"Chargement des noms de famille (INSEE)…"
|
||||
"Chargement des numéros FINESS…"
|
||||
…
|
||||
Puis le splash se ferme et la GUI s'ouvre — pas de fenêtre intermédiaire.
|
||||
|
||||
En mode dev (pas frozen), pyi_splash n'existe pas ; on ajoute un
|
||||
mini-splash tkinter temporaire pour voir le même rendu pendant le test.
|
||||
"""
|
||||
"""Launch the main GUI with visible startup progress."""
|
||||
log.info("Launching GUI...")
|
||||
progress = BrandedSplash(total_steps=5)
|
||||
progress.step("Préparation de l'environnement")
|
||||
|
||||
# Traductions log.info() → libellés "prod" lisibles pour l'utilisateur.
|
||||
_LOG_TRANSLATIONS = [
|
||||
@@ -158,7 +357,7 @@ def launch_gui():
|
||||
class _SplashHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
try:
|
||||
_splash_update(_translate(record.getMessage()))
|
||||
progress.detail(_translate(record.getMessage()))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -167,17 +366,24 @@ def launch_gui():
|
||||
logging.getLogger().addHandler(_handler)
|
||||
|
||||
# Afficher tout de suite un message initial sous le logo
|
||||
_splash_update("Démarrage…")
|
||||
progress.detail("Démarrage du moteur applicatif")
|
||||
|
||||
# Import du core et de la GUI (synchrone : pas besoin de thread puisque
|
||||
# le splash natif tourne dans son propre processus bootloader).
|
||||
result = {"error": None}
|
||||
try:
|
||||
_splash_update("Chargement des dictionnaires médicaux…")
|
||||
progress.step("Chargement des dictionnaires médicaux")
|
||||
import anonymizer_core_refactored_onnx # noqa
|
||||
log.info("Core imported OK")
|
||||
progress.step("Chargement du moteur d'anonymisation")
|
||||
import Pseudonymisation_Gui_V5 # noqa
|
||||
log.info("GUI module imported OK")
|
||||
progress.step("Vérification des modèles locaux")
|
||||
if check_models_ready():
|
||||
progress.detail("CamemBERT-bio ONNX local disponible")
|
||||
else:
|
||||
progress.detail("CamemBERT-bio ONNX non trouvé dans le bundle")
|
||||
progress.step("Ouverture de l'interface")
|
||||
except Exception as e:
|
||||
result["error"] = f"{e}\n{traceback.format_exc()}"
|
||||
log.error(f"Import error: {result['error']}")
|
||||
@@ -188,8 +394,8 @@ def launch_gui():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fermer le splash natif maintenant que tout est prêt
|
||||
_splash_close()
|
||||
# Fermer le splash maintenant que tout est prêt
|
||||
progress.close()
|
||||
|
||||
if result["error"]:
|
||||
try:
|
||||
@@ -239,12 +445,19 @@ class SetupWindow:
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("Anonymisation — Configuration initiale")
|
||||
self.root.geometry("620x450")
|
||||
self.root.geometry("660x700")
|
||||
self.root.resizable(False, False)
|
||||
self._logo_image = None
|
||||
self._log_lines = []
|
||||
|
||||
frame = ttk.Frame(self.root, padding=20)
|
||||
frame = ttk.Frame(self.root, padding=18)
|
||||
frame.pack(fill="both", expand=True)
|
||||
|
||||
splash_path = APP_DIR / "assets" / "splash.png"
|
||||
if splash_path.exists():
|
||||
self._logo_image = tk.PhotoImage(file=str(splash_path))
|
||||
ttk.Label(frame, image=self._logo_image).pack(pady=(0, 8))
|
||||
|
||||
ttk.Label(frame, text="Préparation des modèles d'intelligence artificielle",
|
||||
font=("", 13, "bold")).pack(pady=(0, 4))
|
||||
ttk.Label(
|
||||
@@ -278,6 +491,22 @@ class SetupWindow:
|
||||
font=("", 8)).pack(side="left")
|
||||
self.step_labels[key] = icon
|
||||
|
||||
log_frame = ttk.LabelFrame(frame, text=" Détail du chargement ", padding=8)
|
||||
log_frame.pack(fill="x", pady=(0, 12))
|
||||
self.log_text = tk.Text(
|
||||
log_frame,
|
||||
height=7,
|
||||
wrap="word",
|
||||
state="disabled",
|
||||
bg="#f7f7f7",
|
||||
fg="#333333",
|
||||
bd=0,
|
||||
padx=8,
|
||||
pady=6,
|
||||
font=("Consolas", 8),
|
||||
)
|
||||
self.log_text.pack(fill="x")
|
||||
|
||||
# Bouton relance (caché au début)
|
||||
self.btn = ttk.Button(frame, text="Relancer", command=self.start_download)
|
||||
self.btn.pack(pady=6)
|
||||
@@ -321,43 +550,54 @@ class SetupWindow:
|
||||
try:
|
||||
# 1. EDS-Pseudo
|
||||
self._update("Téléchargement d'EDS-Pseudo… (modèle CamemBERT clinique)")
|
||||
self._append_log("EDS-Pseudo : téléchargement/chargement du modèle AP-HP")
|
||||
self._set_step("eds_pseudo", "running")
|
||||
log.info("Downloading EDS-Pseudo...")
|
||||
try:
|
||||
from eds_pseudo_manager import EdsPseudoManager
|
||||
mgr = EdsPseudoManager()
|
||||
mgr.load()
|
||||
with self._capture_model_output("EDS-Pseudo"):
|
||||
mgr.load()
|
||||
self._set_step("eds_pseudo", "ok")
|
||||
self._append_log("EDS-Pseudo : modèle prêt")
|
||||
log.info("EDS-Pseudo OK")
|
||||
except Exception as e:
|
||||
self._set_step("eds_pseudo", "fail")
|
||||
self._append_log(f"EDS-Pseudo : échec - {e}")
|
||||
failures.append(("EDS-Pseudo", str(e)))
|
||||
log.warning(f"EDS-Pseudo failed: {e}")
|
||||
self._advance()
|
||||
|
||||
# 2. GLiNER
|
||||
self._update("Téléchargement de GLiNER… (détection zero-shot)")
|
||||
self._append_log("GLiNER : téléchargement/chargement du modèle PII")
|
||||
self._set_step("gliner", "running")
|
||||
log.info("Downloading GLiNER...")
|
||||
try:
|
||||
from gliner_manager import GlinerManager
|
||||
mgr = GlinerManager()
|
||||
mgr.load()
|
||||
with self._capture_model_output("GLiNER"):
|
||||
mgr.load()
|
||||
self._set_step("gliner", "ok")
|
||||
self._append_log("GLiNER : modèle prêt")
|
||||
log.info("GLiNER OK")
|
||||
except Exception as e:
|
||||
self._set_step("gliner", "fail")
|
||||
self._append_log(f"GLiNER : échec - {e}")
|
||||
failures.append(("GLiNER", str(e)))
|
||||
log.warning(f"GLiNER failed: {e}")
|
||||
self._advance()
|
||||
|
||||
# 3. CamemBERT-bio ONNX
|
||||
self._update("Vérification CamemBERT-bio ONNX (modèle embarqué)…")
|
||||
self._append_log("CamemBERT-bio ONNX : vérification du modèle embarqué")
|
||||
self._set_step("camembert_onnx", "running")
|
||||
if check_models_ready():
|
||||
self._set_step("camembert_onnx", "ok")
|
||||
self._append_log("CamemBERT-bio ONNX : modèle local présent")
|
||||
else:
|
||||
self._set_step("camembert_onnx", "fail")
|
||||
self._append_log("CamemBERT-bio ONNX : fichier ONNX introuvable")
|
||||
failures.append(("CamemBERT-bio ONNX", "fichier ONNX introuvable dans le bundle"))
|
||||
log.error("CamemBERT-bio ONNX not found")
|
||||
self._advance()
|
||||
@@ -384,6 +624,31 @@ class SetupWindow:
|
||||
def _update(self, msg):
|
||||
self.root.after(0, lambda: self.status_var.set(msg))
|
||||
|
||||
def _append_log(self, msg):
|
||||
clean = " ".join(str(msg).split())
|
||||
if not clean:
|
||||
return
|
||||
if len(clean) > 180:
|
||||
clean = clean[:177] + "..."
|
||||
|
||||
def _apply():
|
||||
self._log_lines.append(clean)
|
||||
self._log_lines = self._log_lines[-80:]
|
||||
self.log_text.configure(state="normal")
|
||||
self.log_text.delete("1.0", tk.END)
|
||||
self.log_text.insert("end", "\n".join(self._log_lines))
|
||||
self.log_text.configure(state="disabled")
|
||||
self.log_text.see("end")
|
||||
|
||||
self.root.after(0, _apply)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _capture_model_output(self, label):
|
||||
stream = ModelProgressStream(self._append_log, label)
|
||||
with contextlib.redirect_stdout(stream), contextlib.redirect_stderr(stream):
|
||||
yield
|
||||
stream.flush()
|
||||
|
||||
def _finish(self):
|
||||
try:
|
||||
self.root.destroy()
|
||||
|
||||
Reference in New Issue
Block a user