698 lines
25 KiB
Python
698 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""Launcher Windows — single-instance + download models on first run + launch GUI."""
|
|
import os
|
|
import sys
|
|
import traceback
|
|
import tkinter as tk
|
|
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
|
|
# 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
|
|
|
|
|
|
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)
|
|
# ---------------------------------------------------------------------------
|
|
_lock_file = None
|
|
_lock_fd = None
|
|
|
|
def _ensure_single_instance():
|
|
"""Prevent multiple instances using a lock file.
|
|
Works reliably on Windows and Linux, including PyInstaller --onefile."""
|
|
global _lock_file, _lock_fd
|
|
import tempfile
|
|
_lock_file = Path(tempfile.gettempdir()) / "anonymisation_chcb.lock"
|
|
try:
|
|
if sys.platform == "win32":
|
|
import msvcrt
|
|
_lock_fd = open(_lock_file, "w")
|
|
msvcrt.locking(_lock_fd.fileno(), msvcrt.LK_NBLCK, 1)
|
|
else:
|
|
import fcntl
|
|
_lock_fd = open(_lock_file, "w")
|
|
fcntl.flock(_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
return True
|
|
except (OSError, IOError):
|
|
return False
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Path resolution for PyInstaller frozen exe
|
|
# ---------------------------------------------------------------------------
|
|
if getattr(sys, 'frozen', False):
|
|
APP_DIR = Path(sys._MEIPASS)
|
|
EXE_DIR = Path(sys.executable).parent
|
|
else:
|
|
APP_DIR = Path(__file__).resolve().parent
|
|
EXE_DIR = APP_DIR
|
|
|
|
# Log file next to the exe
|
|
LOG_FILE = EXE_DIR / "anonymisation.log"
|
|
logging.basicConfig(
|
|
filename=str(LOG_FILE),
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)s %(message)s",
|
|
)
|
|
log = logging.getLogger("launcher")
|
|
|
|
# Make embedded modules importable
|
|
sys.path.insert(0, str(APP_DIR))
|
|
os.chdir(str(APP_DIR))
|
|
|
|
log.info(f"APP_DIR={APP_DIR}")
|
|
log.info(f"EXE_DIR={EXE_DIR}")
|
|
log.info(f"frozen={getattr(sys, 'frozen', False)}")
|
|
|
|
MODELS_DIR = APP_DIR / "models"
|
|
|
|
|
|
def check_models_ready():
|
|
"""Check that the CamemBERT-bio ONNX model is present."""
|
|
onnx_path = MODELS_DIR / "camembert-bio-deid" / "onnx" / "model.onnx"
|
|
ok = onnx_path.exists()
|
|
log.info(f"CamemBERT ONNX present: {ok} ({onnx_path})")
|
|
return ok
|
|
|
|
|
|
def launch_gui():
|
|
"""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 = [
|
|
("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
|
|
|
|
# Handler logs → splash natif. Installé sur le root logger pour capturer
|
|
# tous les log.info() des modules chargés pendant l'import.
|
|
class _SplashHandler(logging.Handler):
|
|
def emit(self, record):
|
|
try:
|
|
progress.detail(_translate(record.getMessage()))
|
|
except Exception:
|
|
pass
|
|
|
|
_handler = _SplashHandler()
|
|
_handler.setLevel(logging.INFO)
|
|
logging.getLogger().addHandler(_handler)
|
|
|
|
# Afficher tout de suite un message initial sous le logo
|
|
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:
|
|
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']}")
|
|
|
|
# Retirer le handler — la GUI principale utilise ses propres logs
|
|
try:
|
|
logging.getLogger().removeHandler(_handler)
|
|
except Exception:
|
|
pass
|
|
|
|
# Fermer le splash maintenant que tout est prêt
|
|
progress.close()
|
|
|
|
if result["error"]:
|
|
try:
|
|
messagebox.showerror(
|
|
"Erreur",
|
|
f"Erreur au lancement :\n\n{result['error'].splitlines()[0]}\n\n"
|
|
f"Voir {LOG_FILE} pour les détails.",
|
|
)
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
# Lancer la fenêtre principale
|
|
try:
|
|
import Pseudonymisation_Gui_V5
|
|
log.info("Starting mainloop…")
|
|
root = tk.Tk()
|
|
Pseudonymisation_Gui_V5.App(root)
|
|
root.mainloop()
|
|
except Exception as e:
|
|
log.error(f"GUI error: {e}\n{traceback.format_exc()}")
|
|
try:
|
|
messagebox.showerror(
|
|
"Erreur",
|
|
f"Erreur de l'interface :\n\n{e}\n\nVoir {LOG_FILE}",
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class SetupWindow:
|
|
"""Setup window for first launch — auto-démarre le téléchargement des modèles.
|
|
|
|
Affiche un suivi détaillé par modèle (EDS-Pseudo, GLiNER, CamemBERT-bio) avec
|
|
indicateurs visuels (⏳ en cours, ✓ succès, ✗ échec). Permet de relancer en
|
|
cas d'erreur. Lancement auto de la GUI une fois tous les modèles prêts.
|
|
"""
|
|
|
|
# Liste ordonnée des étapes de chargement. Chaque entrée :
|
|
# (clé interne, libellé, taille approx, fonction de chargement)
|
|
STEPS = [
|
|
("eds_pseudo", "EDS-Pseudo (CamemBERT clinique)", "~450 Mo"),
|
|
("gliner", "GLiNER (détection PII zero-shot)", "~300 Mo"),
|
|
("camembert_onnx", "CamemBERT-bio ONNX (embarqué)", "local"),
|
|
]
|
|
|
|
def __init__(self):
|
|
self.root = tk.Tk()
|
|
self.root.title("Anonymisation — Configuration initiale")
|
|
self.root.geometry("660x700")
|
|
self.root.resizable(False, False)
|
|
self._logo_image = None
|
|
self._log_lines = []
|
|
|
|
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(
|
|
frame,
|
|
text=("Au premier lancement, les modèles de détection doivent être téléchargés\n"
|
|
"depuis HuggingFace. Cette opération est unique — durée 3 à 10 minutes\n"
|
|
"selon votre connexion internet. Merci de patienter."),
|
|
justify="center", foreground="#555555",
|
|
).pack(pady=(0, 12))
|
|
|
|
# Barre de progression globale
|
|
self.progress = ttk.Progressbar(frame, mode="determinate",
|
|
length=560, maximum=len(self.STEPS))
|
|
self.progress.pack(pady=(0, 4))
|
|
|
|
self.status_var = tk.StringVar(value="Démarrage…")
|
|
ttk.Label(frame, textvariable=self.status_var, foreground="#1a568e").pack(pady=(0, 12))
|
|
|
|
# Zone détail par modèle
|
|
detail_frame = ttk.LabelFrame(frame, text=" Modèles ", padding=10)
|
|
detail_frame.pack(fill="x", pady=(0, 12))
|
|
|
|
self.step_labels = {}
|
|
for key, title, size in self.STEPS:
|
|
row = ttk.Frame(detail_frame)
|
|
row.pack(fill="x", pady=3)
|
|
icon = ttk.Label(row, text="○", width=3, font=("", 12))
|
|
icon.pack(side="left")
|
|
ttk.Label(row, text=title).pack(side="left")
|
|
ttk.Label(row, text=f" ({size})", foreground="#999999",
|
|
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)
|
|
self.btn.pack_forget()
|
|
|
|
# Bouton ignorer/continuer (affiché si échec partiel)
|
|
self.btn_skip = ttk.Button(
|
|
frame, text="Continuer malgré tout",
|
|
command=self._finish,
|
|
)
|
|
self.btn_skip.pack(pady=(0, 4))
|
|
self.btn_skip.pack_forget()
|
|
|
|
# Auto-démarrage du téléchargement (pas besoin de cliquer)
|
|
self.root.after(500, self.start_download)
|
|
|
|
def start_download(self):
|
|
self.btn.pack_forget()
|
|
self.btn_skip.pack_forget()
|
|
self.progress["value"] = 0
|
|
self.status_var.set("Démarrage du téléchargement…")
|
|
for icon in self.step_labels.values():
|
|
icon.configure(text="○", foreground="#999999")
|
|
threading.Thread(target=self._download_thread, daemon=True).start()
|
|
|
|
def _set_step(self, key, state):
|
|
"""state : 'pending' | 'running' | 'ok' | 'fail'"""
|
|
mapping = {
|
|
"pending": ("○", "#999999"),
|
|
"running": ("⏳", "#f57c00"),
|
|
"ok": ("✓", "#2e7d32"),
|
|
"fail": ("✗", "#c62828"),
|
|
}
|
|
char, color = mapping.get(state, ("○", "#999999"))
|
|
icon = self.step_labels.get(key)
|
|
if icon is not None:
|
|
self.root.after(0, lambda: icon.configure(text=char, foreground=color))
|
|
|
|
def _download_thread(self):
|
|
failures = []
|
|
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()
|
|
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()
|
|
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()
|
|
|
|
if failures:
|
|
lines = "\n".join(f" • {name} : {err[:60]}" for name, err in failures)
|
|
self._update(f"Certains modèles ont échoué ({len(failures)}/{len(self.STEPS)}).")
|
|
log.warning(f"Setup partial failure: {len(failures)} model(s) failed\n{lines}")
|
|
self.root.after(0, lambda: self.btn.pack(pady=6))
|
|
self.root.after(0, lambda: self.btn_skip.pack(pady=(0, 4)))
|
|
else:
|
|
self._update("Tous les modèles sont prêts. Lancement de l'interface…")
|
|
log.info("Setup complete, launching GUI in 1.5s")
|
|
self.root.after(1500, self._finish)
|
|
|
|
except Exception as e:
|
|
log.error(f"Setup error: {e}\n{traceback.format_exc()}")
|
|
self._update(f"Erreur inattendue : {e}")
|
|
self.root.after(0, lambda: self.btn.pack(pady=6))
|
|
|
|
def _advance(self):
|
|
self.root.after(0, lambda: self.progress.step(1))
|
|
|
|
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()
|
|
except Exception:
|
|
pass
|
|
launch_gui()
|
|
|
|
def run(self):
|
|
self.root.mainloop()
|
|
|
|
|
|
def main():
|
|
log.info("=== Demarrage Anonymisation ===")
|
|
|
|
# Single-instance check
|
|
if not _ensure_single_instance():
|
|
log.warning("Another instance is already running. Exiting.")
|
|
try:
|
|
messagebox.showwarning(
|
|
"Anonymisation",
|
|
"L'application est deja en cours d'execution.\n\n"
|
|
"Regardez dans la barre des taches.",
|
|
)
|
|
except:
|
|
pass
|
|
sys.exit(0)
|
|
|
|
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:
|
|
log.error(f"Fatal error: {e}\n{traceback.format_exc()}")
|
|
try:
|
|
messagebox.showerror("Erreur fatale", f"{e}\n\nVoir {LOG_FILE}")
|
|
except:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|