feat(gui): apply WIP profils+masques+build-windows from stash (2026-04-27)

Application du stash@{0} resté en WIP depuis le 27/04 :
  "On main: wip-gui-profils-masque-manuel-build-windows-2026-04-27"

## Apport

- Pseudonymisation_Gui_V5.py (+1208 lignes) : profils, panneau paramètres
  avancés, éditeur de masques intégré, gestion whitelist/blacklist
- launcher.py (+315) : splash natif PyInstaller, single-instance,
  téléchargement modèles
- anonymisation_onefile.spec : config PyInstaller mise à jour
- pdf_mask_designer.py (+114) : éditeur de masques amélioré
- config_defaults.py (+23) : constantes nouvelles
- tests/unit/test_config_externalization.py (+12) : tests config
- .gitignore (+5)

## Pourquoi

La version courante de la GUI sur la branche feature manquait :
- L'éditeur de masques
- Les profils
- Le panneau paramètres avancés
- Le splash natif au démarrage

Aucun conflit avec mes 10 commits Q-1 (pas de chevauchement de fichiers).

## Validation

75 passed, 10 xfailed sur pytest tests/unit/.

## Note

Le stash reste disponible dans `git stash list` jusqu'à drop explicite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 11:09:46 +02:00
parent 5d89eaf8dc
commit 380e520013
7 changed files with 1664 additions and 179 deletions

View File

@@ -1,90 +1,128 @@
import os
block_cipher = None
app_dir = 'C:\\Users\\dom\\ai\\anonymisation'
from pathlib import Path
datas = [
(os.path.join(app_dir, 'config'), 'config'),
(os.path.join(app_dir, 'data', 'bdpm'), os.path.join('data', 'bdpm')),
(os.path.join(app_dir, 'data', 'finess'), os.path.join('data', 'finess')),
(os.path.join(app_dir, 'data', 'insee'), os.path.join('data', 'insee')),
(os.path.join(app_dir, 'models', 'camembert-bio-deid', 'onnx'), os.path.join('models', 'camembert-bio-deid', 'onnx')),
(os.path.join(app_dir, 'detectors'), 'detectors'),
(os.path.join(app_dir, 'scripts'), 'scripts'),
# Assets UI : logo (header + splash), icônes fenêtre, splash image.
# Le launcher et la GUI y accèdent via _asset(name) qui résout sous
# sys._MEIPASS/assets en mode frozen.
(os.path.join(app_dir, 'assets'), 'assets'),
]
# Fichiers directs dans data/ — IMPÉRATIF pour fonctionnement correct du core.
# Sans eux : stop-words/villes/DPI labels/companion blacklist sont des sets vides,
# ce qui dégrade la qualité d'anonymisation et peut masquer/laisser passer des faux-positifs.
for data_file in [
'stopwords_manuels.txt',
'villes_blacklist.txt',
'dpi_labels_blacklist.txt',
'companion_blacklist.txt',
block_cipher = None
project_dir = Path(globals().get("SPECPATH", os.getcwd())).resolve()
def _data_entry(relative_path: str, target_dir: str | None = None):
src = project_dir / relative_path
if not src.exists():
return None
return (str(src), target_dir or relative_path)
datas = []
for relative_path, target_dir in [
("config", "config"),
("data/bdpm", "data/bdpm"),
("data/finess", "data/finess"),
("data/insee", "data/insee"),
("models/camembert-bio-deid/onnx", "models/camembert-bio-deid/onnx"),
("detectors", "detectors"),
("scripts", "scripts"),
("assets", "assets"),
]:
src = os.path.join(app_dir, 'data', data_file)
if os.path.exists(src):
datas.append((src, 'data'))
for pyfile in ['anonymizer_core_refactored_onnx.py', 'eds_pseudo_manager.py',
'gliner_manager.py', 'camembert_ner_manager.py',
'Pseudonymisation_Gui_V5.py', 'build_info.py']:
datas.append((os.path.join(app_dir, pyfile), '.'))
entry = _data_entry(relative_path, target_dir)
if entry is not None:
datas.append(entry)
# Fichiers directs sous data/ requis par le core.
for relative_path in [
"data/stopwords_manuels.txt",
"data/villes_blacklist.txt",
"data/dpi_labels_blacklist.txt",
"data/companion_blacklist.txt",
]:
entry = _data_entry(relative_path, "data")
if entry is not None:
datas.append(entry)
hiddenimports = [
"Pseudonymisation_Gui_V5",
"anonymizer_core_refactored_onnx",
"admin_rules",
"config_defaults",
"profile_defaults",
"gui_batch_paths",
"manual_masking",
"pdf_mask_designer",
"format_converter",
"ner_manager_onnx",
"camembert_ner_manager",
"eds_pseudo_manager",
"gliner_manager",
"vlm_manager",
"build_info",
"doctr",
"doctr.io",
"doctr.models",
"doctr.models.detection",
"doctr.models.recognition",
"cv2",
"torchvision",
"edsnlp",
"edsnlp.pipes",
"edsnlp.pipes.ner",
"edsnlp.pipes.ner.pseudo",
"spacy",
"spacy.lang.fr",
"gliner",
"onnxruntime",
"transformers",
"tokenizers",
"torch",
"pdfplumber",
"fitz",
"PIL",
"yaml",
"loguru",
"regex",
"optimum",
"optimum.onnxruntime",
"optimum.pipelines",
"optimum.modeling_base",
"optimum.exporters.onnx",
]
a = Analysis(
[os.path.join(app_dir, 'launcher.py')],
pathex=[app_dir],
[str(project_dir / "launcher.py")],
pathex=[str(project_dir)],
datas=datas,
hiddenimports=[
'anonymizer_core_refactored_onnx', 'eds_pseudo_manager',
'gliner_manager', 'camembert_ner_manager', 'Pseudonymisation_Gui_V5',
'edsnlp', 'edsnlp.pipes', 'edsnlp.pipes.ner', 'edsnlp.pipes.ner.pseudo',
'spacy', 'spacy.lang.fr', 'gliner', 'onnxruntime',
'transformers', 'tokenizers', 'torch', 'pdfplumber',
'ahocorasick', 'sklearn', 'scipy', 'pydantic', 'yaml', 'PIL',
'loguru', 'regex',
# optimum : utilisé par ner_manager_onnx.py (fallback NER legacy).
# Sans ça, la GUI affiche "NER indisponible : optimum.onnxruntime introuvable"
# si EDS-Pseudo échoue. Le pipeline principal (CamemBERT-bio ONNX +
# EDS-Pseudo + GLiNER) n'en dépend pas — mais l'absence du hiddenimport
# crée un message d'erreur cosmétique gênant.
'optimum', 'optimum.onnxruntime', 'optimum.pipelines',
'optimum.modeling_base', 'optimum.exporters.onnx',
],
hiddenimports=hiddenimports,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
# Splash natif PyInstaller : image affichée AU LANCEMENT DE L'EXE,
# avant même que Python démarre. Couvre les ~15-30 s de décompression
# du bundle --onefile dans %TEMP% qui laissaient l'écran vide auparavant.
# Le launcher ferme le splash via pyi_splash.close() une fois la GUI prête.
splash = Splash(
os.path.join(app_dir, 'assets', 'splash.png'),
str(project_dir / "assets" / "splash.png"),
binaries=a.binaries,
datas=a.datas,
# Texte dynamique PyInstaller positionné dans la zone libre du PNG
# (y=170-235). text_pos correspond au coin haut-gauche du texte.
text_pos=(60, 195),
text_size=10,
text_color='white',
text_color="white",
minify_script=True,
always_on_top=False,
)
exe = EXE(
pyz, a.scripts,
splash, # image affichée immédiatement
splash.binaries, # bootloader splash
a.binaries, a.zipfiles, a.datas, [],
name='Anonymisation',
pyz,
a.scripts,
splash,
splash.binaries,
a.binaries,
a.zipfiles,
a.datas,
[],
name="Anonymisation",
debug=False,
strip=False,
upx=False,
console=False,
# Icône du fichier .exe visible dans l'Explorateur Windows et la taskbar
# (dérivée du logo aivanonym, multi-résolution 16→256 dans le .ico).
icon=os.path.join(app_dir, 'assets', 'icons', 'app.ico'),
icon=str(project_dir / "assets" / "icons" / "app.ico"),
)