#!/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 # --------------------------------------------------------------------------- # 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.""" log.info("Launching GUI...") try: import anonymizer_core_refactored_onnx # noqa — pre-import the core log.info("Core imported OK") except Exception as e: log.error(f"Core import error: {e}\n{traceback.format_exc()}") try: import Pseudonymisation_Gui_V5 # noqa log.info("GUI imported OK, 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 au lancement de l'interface:\n\n{e}\n\nVoir {LOG_FILE}") except: pass class SetupWindow: """Setup window for first launch (model download).""" def __init__(self): self.root = tk.Tk() self.root.title("Anonymisation — Premier lancement") self.root.geometry("520x320") self.root.resizable(False, False) frame = ttk.Frame(self.root, padding=20) frame.pack(fill="both", expand=True) ttk.Label(frame, text="Configuration initiale", font=("", 14, "bold")).pack(pady=(0, 10)) ttk.Label( frame, text="Les modeles NER vont etre telecharges depuis HuggingFace.\nCela peut prendre quelques minutes selon la connexion.", justify="center", ).pack(pady=(0, 15)) self.progress = ttk.Progressbar(frame, mode="indeterminate", length=420) self.progress.pack(pady=10) self.status_var = tk.StringVar(value="En attente...") ttk.Label(frame, textvariable=self.status_var).pack(pady=5) self.btn = ttk.Button(frame, text="Demarrer le telechargement", command=self.start_download) self.btn.pack(pady=15) def start_download(self): self.btn.configure(state="disabled") self.progress.start(10) threading.Thread(target=self._download_thread, daemon=True).start() def _download_thread(self): try: # 1. EDS-Pseudo self._update("Chargement EDS-Pseudo (telechargement HuggingFace)...") log.info("Downloading EDS-Pseudo...") try: from eds_pseudo_manager import EdsPseudoManager mgr = EdsPseudoManager() mgr.load() self._update("EDS-Pseudo OK") log.info("EDS-Pseudo OK") except Exception as e: self._update(f"EDS-Pseudo: {e}") log.warning(f"EDS-Pseudo failed: {e}") # 2. GLiNER self._update("Chargement GLiNER (telechargement HuggingFace)...") log.info("Downloading GLiNER...") try: from gliner_manager import GlinerManager mgr = GlinerManager() mgr.load() self._update("GLiNER OK") log.info("GLiNER OK") except Exception as e: self._update(f"GLiNER: {e}") log.warning(f"GLiNER failed: {e}") # 3. CamemBERT-bio ONNX self._update("Verification CamemBERT-bio ONNX...") if check_models_ready(): self._update("CamemBERT-bio ONNX OK") else: self._update("CamemBERT-bio ONNX manquant") log.error("CamemBERT-bio ONNX not found") self._update("Configuration terminee ! Lancement de l'interface...") log.info("Setup complete, launching GUI in 2s") self.root.after(2000, self._finish) except Exception as e: log.error(f"Setup error: {e}\n{traceback.format_exc()}") self._update(f"Erreur: {e}") self.root.after(0, lambda: self.btn.configure(state="normal")) def _update(self, msg): self.root.after(0, lambda: self.status_var.set(msg)) def _finish(self): self.progress.stop() self.root.destroy() 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(): launch_gui() else: 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()