From d1bdfb1aca99122bb2f9cba3b860de73e3abab38 Mon Sep 17 00:00:00 2001 From: Domi31tls Date: Wed, 18 Mar 2026 00:51:54 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20fen=C3=AAtres=20fant=C3=B4mes=20PyInstal?= =?UTF-8?q?ler=20=E2=80=94=20d=C3=A9sactiver=20ProcessPoolExecutor=20en=20?= =?UTF-8?q?mode=20frozen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProcessPoolExecutor relançait l'exe pour chaque sous-processus de rastérisation sous PyInstaller --onefile, créant une fenêtre GUI par page. En mode frozen, la rastérisation est maintenant séquentielle. Aussi: remplacement du mutex Windows par un file lock (msvcrt.locking) plus fiable pour la protection anti-multi-instance. Co-Authored-By: Claude Opus 4.6 (1M context) --- anonymizer_core_refactored_onnx.py | 11 +- launcher.py | 222 +++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 launcher.py diff --git a/anonymizer_core_refactored_onnx.py b/anonymizer_core_refactored_onnx.py index e16466f..adf2b2d 100644 --- a/anonymizer_core_refactored_onnx.py +++ b/anonymizer_core_refactored_onnx.py @@ -3583,15 +3583,20 @@ def redact_pdf_raster(original_pdf: Path, audit: List[PiiHit], out_pdf: Path, dp image_rects_by_page[pno] = img_rects doc.close() # fermer AVANT le fork - n_workers = min(n_pages, os.cpu_count() or 4) tasks = [ (str(original_pdf), pno, rects_as_tuples.get(pno, []), dpi, ogc_label, jpeg_quality, image_rects_by_page.get(pno, [])) for pno in range(n_pages) ] - with ProcessPoolExecutor(max_workers=n_workers) as pool: - results = sorted(pool.map(_rasterize_page, tasks), key=lambda x: x[0]) + # Mode frozen (PyInstaller --onefile) : ProcessPoolExecutor relance l'exe + # et ouvre des fenêtres GUI fantômes → séquentiel obligatoire + if getattr(sys, 'frozen', False) or n_pages <= 2: + results = sorted([_rasterize_page(t) for t in tasks], key=lambda x: x[0]) + else: + n_workers = min(n_pages, os.cpu_count() or 4) + with ProcessPoolExecutor(max_workers=n_workers) as pool: + results = sorted(pool.map(_rasterize_page, tasks), key=lambda x: x[0]) # Assemblage final (séquentiel, rapide) out = fitz.open() diff --git a/launcher.py b/launcher.py new file mode 100644 index 0000000..4a2d0f2 --- /dev/null +++ b/launcher.py @@ -0,0 +1,222 @@ +#!/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()