## Bannière mode admin
Ajout d'un suffixe "[⚙ MODE ADMIN]" dans le titre de la fenêtre principale
quand `admin_mode.is_admin()` retourne True. Signal visuel clair pour :
- Le bêta-testeur (s'il bidouille, il voit qu'il a déverrouillé quelque chose)
- L'opérateur Dom (pour vérifier d'un coup d'œil que le mode admin est actif
pour ses propres tests)
## Périmètre D-13 partial
Documenté dans `decisions/2026-06-02_dom_d13-partial-scope.md` :
| Protection | Statut |
|---|---|
| VLM Ollama caché en non-admin | ✅ (D-11) |
| Titre fenêtre signalé en admin | ✅ (ce commit) |
| Stopwords personnalisés | ⏭ Reporté v11.5 |
| Profils techniques (regex_overrides, force_terms) | ⏭ Reporté v11.5 |
| Choix moteur NER | ⏭ Reporté v11.5 |
| Sauvegarde configs sensibles | ⏭ Reporté v11.5 |
## Pourquoi le report est OK pour MVP
1. Le risque RGPD critique (envoi externe à Ollama) est résolu par D-11
2. Les autres réglages, bien que visibles, ne déclenchent pas de fuite
3. La transposition customtkinter v6 (v11.5) refondra l'UI — patcher
2874 lignes tkinter aujourd'hui = double travail à refaire en v6
4. Le bêta-testeur n'a pas accès au mode admin (pas de fichier .admin
livré, pas d'env var par défaut)
## Activation manuelle
- Env : `ANON_ADMIN=1 python Pseudonymisation_Gui_V5.py`
- Fichier : créer `.admin` à la racine
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Module admin_mode.py
Nouveau module qui détecte si l'application tourne en mode admin :
- Variable d'environnement `ANON_ADMIN=1` (ou `true`/`yes`/`on`)
- OU fichier `.admin` à la racine de l'application
Expose :
- `is_admin()` — retourne bool, caché en module
- `admin_required(feature_name)` — garde qui lève RuntimeError si pas admin
Pas de mot de passe — c'est un verrou "interdit aux distraits" pour ne
pas exposer au bêta-testeur des options sensibles (envoi à Ollama, conf
critique). Le vrai durcissement viendra avec D-13 (mode admin complet).
## GUI — VLM Ollama caché par défaut (D-11)
Dans Pseudonymisation_Gui_V5.py, après l'import classique de VlmManager,
on force VlmManager = None et VlmConfig = None **si le mode admin n'est
pas actif**.
Effet :
- Bêta-testeur lambda : VLM Ollama complètement invisible et inactif
(économise aussi la RAM du modèle CamemBERT-bio + downloads Ollama)
- Mode admin activé : comportement actuel inchangé
Tests manuels :
- import GUI sans env : VlmManager = None ✅
- `ANON_ADMIN=1 python -c "import Pseudonymisation_Gui_V5"` : VlmManager
est <class 'vlm_manager.VlmManager'> ✅
## Reste à faire (D-13)
- Mode admin = mot de passe / fingerprint
- Cacher dans l'UI les widgets liés au VLM (cases à cocher, etc.)
- Cacher d'autres réglages sensibles (stopwords personnalisés,
regex_overrides, force_terms)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sur le corpus FC, "DAS" était détecté comme nom de famille INSEE en
contexte fort (suivi de "DR") et compté comme leak audit par le scoring.
En réalité, DAS est un **acronyme PMSI / T2A** :
- DP = Diagnostic Principal
- DR = Diagnostic Relié
- **DAS = Diagnostic Associé Significatif**
Contexte typique :
DR
DAS
Actes
Rappel : un code CIM de DAS suivi d'un astérisque correspond à
une CMA exclue par le DP
Le pipeline pensait "Dr. DAS" = médecin nommé DAS. Ajout de "das" aux
stopwords pour bloquer la détection.
Risque résiduel : si un vrai patient/médecin nommé DAS existe, il ne
sera pas masqué. C'est un trade-off acceptable car le PMSI utilise DAS
partout dans les rapports T2A.
Impact attendu : score qualité FC remonte 99.3 → ~100/100 (1 leak audit
fictif éliminé).
Découverte par Qwen dans son audit du 2026-06-02 14:50.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Intégration de paranames (bltlab/paranames v2024.05.07.0, CC BY 4.0)
pour étendre la couverture du gazetteer aux noms étrangers en France
absents d'INSEE (basques, maghrébins, asiatiques, africains, etc.).
## Citation
Sälevä, J., & Lignos, C. (2024). ParaNames 1.0: Creating an Entity Name
Corpus for 400+ Languages using Wikidata. In Proceedings of LREC-COLING
2024. https://aclanthology.org/2024.lrec-main.1103/
## Fichiers
- scripts/build_paranames_gazetteer.py — script reproductible
- data/paranames/README.md — attribution + procédure
- data/paranames/EXTRACTION.md — workflow reproductible
- data/paranames/noms_famille_world.txt.gz — 1 379 609 noms (4.3 Mo gz, <30 Mo RAM)
- data/paranames/prenoms_world.txt.gz — 502 302 prénoms (1.4 Mo gz)
## Volume final
Réduction significative vs estimation initiale (~80 Mo) grâce à NFKD+A-Z
qui fusionne toutes les translittérations Wikidata (cyrilliques, arabes,
chinoises…) en latin de base. Résultat : 4.3 Mo gz total, ~30 Mo RAM.
## Spot-check
| Nom | Présent ? | Note |
|---|---|---|
| EJNAINI | ✅ | Le cas de fuite résiduelle audit_30 — devrait être fixé |
| OYARZABAL | ✅ | Variante basque |
| OYARCABAL | ❌ | Orthographe franco-espagnole rare, absente Wikidata |
| NGUYEN, SCHMIDT, OBAMA, NAKAMURA, GARCIA, MARTIN, BERNARD | ✅ | OK |
## Intersection INSEE
- ∩ INSEE FR : 130 340 noms (59.5 % de couverture INSEE)
- Gain net : 1 249 269 noms supplémentaires (focus diaspora / DOM-TOM)
## Risque FP identifié
Quelques mots français courants sont présents dans paranames (origine :
noms d'autres langues) : VOIR, ALLO. MIDI déjà filtré par stopwords.
Impact à mesurer sur retraitement audit_30. Si nécessaire, ajout d'un
filtre dictionnaire français à apporter ultérieurement.
## Source
- Dépôt : https://github.com/bltlab/paranames
- Mirror HF (utilisé) : https://huggingface.co/datasets/imvladikon/paranames
- License : CC BY 4.0
- Origine : Wikidata (entités publiques) — pas de PII fuitée
REJETÉ comme alternative : philipperemy/name-dataset (origine = leak
Facebook 2021, RGPD bloquant pour produit médical).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Préparation à l'intégration du gazetteer paranames (Wikidata CC BY 4.0,
Sälevä & Lignos LREC-COLING 2024) qui couvrira les noms étrangers en
France absents du gazetteer INSEE (basques, maghrébins, asiatiques,
africains, etc.).
## Loader
- `_PARANAMES_NOMS_SET` + `_PARANAMES_LOADED` (cache global)
- `_load_paranames_noms()` : lazy load au 1er besoin
- Fichier cible : `data/paranames/noms_famille_world.txt.gz`
- Si fichier absent : retourne set vide, log INFO, comportement actuel
(INSEE seul) — fallback transparent
- Si erreur de lecture : log WARNING, fallback INSEE
## Intégration cross-validation
Dans `_cross_validate_name_candidates`, `is_in_insee` étendu :
is_in_insee = (tok_upper in insee_noms or tok_upper in insee_prenoms
or tok_upper in _load_paranames_noms())
Effets :
- En contexte "low" + non NER : un token comme OYARCABAL (basque) ou
EJNAINI (maghrébin) sera désormais accepté si présent dans paranames.
- Aucun changement pour noms FR (déjà dans INSEE).
- Aucune régression : si le fichier paranames n'est pas généré, le
comportement est strictement identique.
## Génération du gazetteer
Le script de génération `scripts/build_paranames_gazetteer.py` et le
fichier `data/paranames/noms_famille_world.txt.gz` sont produits par un
agent dédié en cours d'exécution. Commit séparé à venir avec :
- Script de génération
- README + attribution CC BY 4.0
- Fichier gazetteer
## Tests
74 passed sur 75 (1 test happy path Q-1) + 10 xfailed. 5 tests
synthetic_review cassés (non liés à ce commit — issue séparée du
CHCB cleanup à fixer dans un commit dédié).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L'agent CHCB cleanup a remplacé CHCB → CHUXX dans le path SOURCE_ROOT
mais le vrai dossier sur le disque Dom s'appelle bien
'II-1 Ctrl_T2A_2025_CHCB_DocJustificatifs (1)'. Ça a cassé toutes
les recherches PDF (29/29 MISSING).
Fix : lecture du path depuis env var ANON_AUDIT30_SOURCE avec fallback
sur le path local réel. Le nom CHCB est dans le path filesystem chez
Dom, pas une référence sémantique à anonymiser.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Complète F3 (qui captait le nom APRÈS "Nom usuel :"). Dans certains
comptes-rendus type BACTERIO, l'identité patient sous forme
"NAME Prenom1 Prenom2" apparaît juste AVANT le label, sans label devant.
Cas typique BACTERIO 23232115 :
10.40
SIMONET Marie lise ← cette ligne, pas attrapée par F3
Nom usuel :
14/03/1985
OYARCABAL ← capturée par F3
Ajout de RE_EXTRACT_NAME_BEFORE_NOM_USUEL qui regarde la ligne
précédant directement le label "Nom usuel :" : si elle ressemble à
"MAJUSCULES Prenom Prenom" (NAME ≥4 chars + 1 à 3 tokens
en suite), on la capture en contexte "high" (champ DPI quasi-certain).
Validation sur exemple synthétique :
- F3 OYARCABAL : ['OYARCABAL'] ✅
- F2 SIMONET : ['SIMONET Marie lise'] ✅
Reste à valider sur retraitement audit_30 complet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-applique les remplacements dans anonymizer_core_refactored_onnx.py
(commentaires reverted par un linter entre les commits) et corrige
docs/coordination/inbox/for-dom/2026-06-02_qwen_owncloud-livraison-procedure.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Étend .gitignore pour exclure les répertoires de travail contenant des
données patient réelles (corpus_validation/, regression_tests/baseline/,
tests/ground_truth/, tests/phase1_production_test/, data/silver_annotations/*.bio,
test_chcb_leak/, test_3ogc/, test_anonymise/, test_gui_output/).
Retire ces fichiers du suivi git (git rm --cached) sans les supprimer du
disque local. Conforme à la décision D-12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anonymise les références aux entités réelles (CHCB, villes basques,
Saint-Denis, Réunion, etc.) dans la documentation projet, les maquettes
HTML/Python, les notes de coordination et les audits.
Conserve docs/coordination/decisions/2026-06-02_dom_mvp-pivots-strategiques.md
(table de mapping de référence) et docs/coordination/inbox/for-claude/
intacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le pipeline ne reconnaissait pas le label "Nom usuel :" — utilisé dans
certains comptes-rendus type BACTERIO. Ajout d'une regex dédiée
RE_EXTRACT_NOM_USUEL qui :
1. Trouve "Nom usuel :" en début de ligne
2. Skippe les lignes qui ne commencent pas par une lettre majuscule
(date au format DD/MM/YYYY, placeholders entre crochets, lignes vides)
3. Capture le premier token en MAJUSCULES ≥4 chars
Cas couvert : BACTERIO 23232115 contient
SIMONET Marie lise
Nom usuel :
14/03/1985
OYARCABAL
OYARCABAL est ainsi extrait avec contexte "high" (champ DPI structuré
quasi-certain) et masqué.
Test unitaire rapide validé sur l'exemple ci-dessus.
Reste à faire : F2 (SIMONET — pattern NAME+PRENOM+PRENOM sans label) — non
trivial sans label, à implémenter avec heuristique contextuelle (top du doc,
etc.). Reporté.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Petit utilitaire pour re-traiter le corpus audit_30 avec le code courant
et générer un dossier de sortie horodaté.
Usage:
python scripts/reprocess_audit30.py [--out /tmp/.../foo] [--no-ner]
Lit la liste des 29 docs depuis evaluation/baseline_scores.json, retrouve
chaque PDF source dans /home/dom/Téléchargements/.../CHCB_DocJustificatifs,
appelle process_pdf() pour chacun, sortie dans /tmp/reprocess_audit30/
(ou --out).
Permet ensuite de mesurer la qualité avec :
python scripts/evaluate_quality.py --dir <output> --compare
Validé sur audit_30 — 29 docs en ~4 min avec NER ONNX.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## F1 — Décomposition noms composés (corrige GRAND, EJNAINI)
Quand le NER détecte un nom à trait d'union (ex "Romain BILLON-GRAND",
"Cécilia NOCENT-EJNAINI"), le regex `\bBILLON-GRAND\b` ne traverse pas le
saut de ligne du formatage Trackare en colonnes étroites ("BILLON-\nGRAND").
Solution dans `_apply_extracted_names` : pour chaque nom validé contenant un
`-` (et ≥5 chars), ajouter aussi les sous-tokens (≥4 chars) à `safe_names`.
Les sous-tokens héritent du `bypass_stopwords` du composé (cas Dr/Mme).
Validation sur audit_30 :
- GRAND : 17 → 0 occurrences ✅
- Score global : 97.9 → 98.3 (+0.4)
- leak_audit : 3 → 1
## F4 — Filet rescan résiduel élargi noms INSEE (OPT-IN)
Le rescan post-anonymisation ne couvrait que NIR/EMAIL/IBAN/TEL. Ajout
d'un check sur les tokens uppercase ≥4 chars présents dans le gazetteer
INSEE (`_INSEE_NOMS_FAMILLE`), hors stopwords médicaux, hors placeholders,
hors whitelist utilisateur.
**Désactivé par défaut** (`cfg["rescan"]["check_insee_names"] = False`).
Raison : INSEE contient beaucoup de mots français courants (VOIR, ALLO,
POLYGONE, MIDI, FAURE, …) qui produisent un sur-masquage massif. Sur le
corpus audit_30, F4 activé met 29/29 docs en quarantaine. Inutilisable
en l'état mais utile pour un futur profil "paranoid" avec filtre par
fréquence INSEE rare + dictionnaire français en exclusion.
À activer via :
cfg["rescan"]["check_insee_names"] = True
## Restant
- F2 (SIMONET) : pattern NAME+PRENOM+PRENOM → medium (à implémenter)
- F3 (OYARCABAL) : label "Nom usuel :" → high sur ligne suivante (à implémenter)
- EJNAINI : mystère — fix F1 devrait suffire mais ne suffit pas, à investiguer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Fichiers déplacés (git mv, historique préservé)
- Pseudonymisation_Gui_Models_V4.py (V4 obsolète)
- pseudonymisation_pipeline_gui_v3.py (V3 obsolète)
- Pseudonymisation_Pipeline_Robuste_Patch.py (oct 2025, abandonné)
- pseudonymisation_pipeline_robuste.py (oct 2025, abandonné)
- test_gui_error.py (test orphelin V4)
- test_gui_fixed.py (test orphelin V4)
## Pourquoi
Pour éviter toute confusion avec la GUI active (Pseudonymisation_Gui_V5.py)
maintenant que le stash WIP 2026-04-27 (profils + masques + build windows)
a été appliqué et que Dom va y faire des modifications avant le MVP.
## README ajouté
archives/legacy_gui/README.md documente le contenu, les raisons d'archivage,
les fichiers actifs en production, et la procédure de restauration.
## Restauration
Réversible via : git mv archives/legacy_gui/<file> .
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Implémentation de la traçabilité B-1 sur les sorties d'anonymisation.
## .audit.jsonl — entrée metadata en 1ère ligne
Chaque .audit.jsonl commence maintenant par une entrée :
{"type": "metadata",
"app_version": "0.11.0-mvp",
"build_date": "...",
"build_commit": "...",
"build_branch": "...",
"processed_at": "<iso>",
"document_name": "...",
"ocr_used": bool,
"extracted_chars": int,
"quarantine_flags": []}
Permet de prouver a posteriori avec quelle config un document a été
anonymisé (audit DPO / CNIL).
## XMP PDF — _apply_pseudo_xmp_metadata()
Helper appelé avant doc.save() dans redact_pdf_vector et redact_pdf_raster :
1. doc.set_metadata({}) — efface TOUTES les métadonnées source
(CRITIQUE : les PDF source peuvent contenir le nom patient dans
/Author, /Title, /Keywords)
2. Pose nos métadonnées : creator/producer "Pseudonymisation v...",
title="Document anonymise", author vide, keywords avec commit+ts
3. Garde-fou : log + overwrite si une métadonnée source survit
(defense in depth)
## Constantes module-level
- APP_VERSION = "0.11.0-mvp" (à incrémenter avant chaque rebuild release)
- BUILD_DATE/BUILD_COMMIT/BUILD_BRANCH chargés depuis build_info.py
(regénéré à chaque rebuild EXE). Fallback "dev/unknown" en dev.
## Tests
74 passed, 10 xfailed — pas de régression.
Ref: docs/coordination/inbox/for-dom/2026-05-29_consolide_pseudocode-Q1-v2.md §7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Étape E du sprint Q-1 — B-3 pré-flight.
Si extract_text_with_fallback_ocr retourne moins de SEUIL_TEXTE_MINI
(=100) caractères :
- log.warning systématique
- Si quarantine_mgr fourni : flag preflight_text_too_short (severity=full),
copie du PDF original dans quarantine_dir/ pour ré-essai manuel
- Return {} (pas de sortie texte/audit/PDF pour ce doc)
Couvre les cas : scan non-OCRisé, PDF vide, OCR raté.
Évite le pire scénario : un opérateur qui croit que son document est
anonymisé alors qu'aucune PII n'a même été détectée parce qu'il n'y
avait pas de texte à traiter.
Rétro-compat préservée : sans quarantine_mgr, le comportement reste
"return {}" + log au lieu du silence (toujours strictement meilleur).
Risque appelants : un caller qui suppose la présence des clés "text"/
"audit" dans le retour doit gérer le cas dict vide. À voir au runtime.
Ref: docs/coordination/inbox/for-dom/2026-05-29_consolide_pseudocode-Q1-v2.md §8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Étape D3 du sprint Q-1 (sous-commit 3/3 pour process_pdf, finalise D).
Décision B du consolidé v2 : fallback raster SYSTÉMATIQUE (option 3a
validée par Dom). Si redact_pdf_vector rate :
1. Tente redact_pdf_raster avec les mêmes paramètres
2. Si raster OK :
- outputs["pdf_raster"] est rempli
- flag pdf_vector_fallback_to_raster (severity=partial) → signale
au DPO que le PDF livré est en qualité raster (moins précis)
3. Si raster rate aussi :
- flag pdf_redaction_failed avec détail des 2 erreurs
4. Décision A finalisée : si quarantine_mgr fourni, le .pseudonymise.txt
est copié dans quarantine_dir/ pour autoportance opérateur
(un seul dossier à consulter au lieu de naviguer entre 2)
Import ajouté : shutil (stdlib).
Rétro-compat préservée : si quarantine_mgr is None, le fallback raster
est tenté quand même (RGPD-friendly), mais sans flag ni copie texte.
Le bloc "also_make_raster_burn" qui suit reste inchangé — un appelant
qui veut un raster systématique en plus du vector continue de le forcer
via ce flag.
Ref: docs/coordination/inbox/for-dom/2026-05-29_consolide_pseudocode-Q1-v2.md §3 Décisions A+B, §10
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Étape D2 du sprint Q-1 (sous-commit 2/3 pour process_pdf) :
Avant : try/except Exception: pass sur redact_pdf_vector → le PDF
n'était pas généré mais l'opérateur n'en savait rien.
Maintenant :
- log.warning systématique de l'échec (rétro-compat : même si
quarantine_mgr is None, on log)
- Si quarantine_mgr fourni : flag pdf_redaction_failed (severity=partial)
- Le texte .pseudonymise.txt est déjà sorti avant ce bloc, donc on
ne raise pas — le doc sort en quarantaine partielle propre
Le fallback raster + copie texte en quarantaine pour autoportance
arrivent en D3.
Rétro-compat préservée : les appels actuels sans quarantine_mgr
voient seulement une nouvelle ligne de log.warning au lieu du silence.
Ref: docs/coordination/inbox/for-dom/2026-05-29_consolide_pseudocode-Q1-v2.md §1 cas #6, §3 Décision A
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avant : silence sur apply_redactions échec → PDF sortait sans
rédaction (fuite RGPD critique en milieu santé).
Maintenant : log.warning + raise → l'exception remonte à
process_pdf qui la traitera en étape D (try/flag Q-PDF).
Note transitoire : tant que process_pdf:4655 a encore
'except: pass', le comportement net est "PDF non généré
silencieusement". C'est strictement meilleur qu'avant (pas
de fuite) mais pas encore optimal (pas d'alerte opérateur).
L'étape D complète la chaîne avec QuarantineManager.flag().
Ref: docs/coordination/inbox/for-dom/2026-05-29_consolide_pseudocode-Q1-v2.md §1 cas #5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le mot "grand" en stopword filtrait les noms INSEE valides
comme GRAND, BILLON-GRAND lors du masquage NER. Sur le corpus
audit_30 : 17 fuites du nom "GRAND" dans
trackare-05012965-23060770.
Fix : suppression de la ligne (pipeline INSEE exige contexte
fort pour masquer, "grand" minuscule isolé ne sera pas FP).
Tests à venir : tests/unit/test_c8_grand_regression.py (Qwen)
Ref: docs/coordination/inbox/for-dom/2026-05-29_qwen_analyse-regression-grand.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fix(detect): EMAIL masqué avant _apply_overrides pour éviter que les
force_terms (ex: CHCB) ne cassent l'adresse — mh.lafitte@chcb.fr → [EMAIL]
- fix(corpus): expected 007 mis à jour ([EMAIL] à la place de mh.[NOM]@[MASK].fr)
- feat(tools): tools/simulate_admin_rule.py — CLI de simulation et validation
isolée d'une règle admin (--text, --file, --corpus, --all)
- fix(admin_rules): required_case_ids corrigés dans admin_rules.default.yml
(noms des répertoires du corpus synthétique mis à jour)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trois fixes qui font passer 009_multi_etablissements en vert et
ferment la liste des fuites identifiées par la couche 2.
#3 — `Centre Hospitalier Universitaire de Bordeaux` coupé sur deux lignes
Nouveau pattern `RE_ETAB_LINEBREAK` (strict) en pré-passe sur la page
entière, juste avant le découpage en lignes. Match `<TYPE>\n<suite>`
avec :
- TYPE limité (Centre Hospitalier, Hôpital, Clinique, Polyclinique,
CHU, CHRU, CHS) ;
- un seul `\n` autorisé entre TYPE et suite ;
- la suite démarre obligatoirement par un connecteur typique
(Universitaire, de, d', du, des, la, le, les) puis UN nom propre.
Évite le FP `CENTRE HOSPITALIER COTE BASQUE\nService d'anesthésie`
(le `\n` n'est pas immédiat après le type, donc pas de match).
#4 — `CHCB` en fin de phrase suivi de ` ;`
`_kv_value_only_mask` splittait `transféré au CHCB pour la rééducation ;`
sur le `;` du `SPLITTER` (`\s*[:|;\t]\s*`), produisant une value vide.
La key contenait CHCB mais n'était passée qu'à `_mask_critical_in_key`
qui ne couvre pas les force_terms admin_rules.
Fix : fallback sur `_mask_line_by_regex(line)` (qui appelle
`_apply_overrides` → force_terms) si la value est vide ou la key
dépasse 5 mots (heuristique narrative).
#5 — `Biarritz` non masqué après `[ETABLISSEMENT] à Biarritz`
`_mask_ville_gazetteers` skippait par sécurité toute ville détectée
juste après un placeholder établissement précédé de `de/du/d'/à`. Le
`à` était inclus pour éviter les FP, mais c'est la préposition de
LOCALISATION par excellence : `Clinique Aguilera à Biarritz` perd
Biarritz à tort. Restreint le skip à `de/du/d'` (qui sont des parties
de nom d'établissement type `CHU de Bordeaux`). `à` reste actif.
Couche 2 entièrement verte : 73 passed, 0 xfailed (avant : 72 + 1
xfailed). KNOWN_FAILURES vidé. La gate pytest est désormais le
contrat de non-régression sur 10 documents complets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Étend `RE_RPPS` pour tolérer 0 à 3 mots qualificateurs entre `RPPS`
et le séparateur `:` ou `-`. Couvre les variantes observées :
- RPPS prescripteur :
- RPPS du médecin signataire :
- RPPS de garde -
- N° RPPS :
Si un qualificateur est présent, le séparateur (`:` ou `-`) devient
obligatoire pour éviter d'aspirer du narratif (faux positif type
"Le RPPS est consulté pour vérifier 12345678901 dans la base").
La lambda `_repl_rpps` reconstruit `RPPS : [RPPS]` en sortie : le
qualificateur est consommé mais perdu (pas de fuite, choix cosmétique).
Cas 005_bacterio_complete passe désormais (retiré de KNOWN_FAILURES).
La fuite `10101010101` derrière `RPPS prescripteur :` est masquée.
Cohérent avec le cadrage section 10.1 (règle cœur générique
applicable à tout établissement de santé français — pas de
spécificité locale).
Tests : 72 passed, 1 xfailed (avant : 71 passed, 2 xfailed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trois nouveaux patterns cœur dans `_mask_structured_line` pour des
labels génériques qui n'étaient pas couverts par le pipeline kv_value
(le split key:value laissait fuir la valeur quand le label dépassait
les patterns existants `RE_EXTRACT_NOM_NAISSANCE`, `RE_EXTRACT_PRENOM`,
`RE_EXTRACT_VILLE_RESIDENCE`).
`RE_LABEL_NOM_VARIANTES` capture :
- Nom de jeune fille / de famille / de naissance(.)
- Nom d'usage / Nom marital / Nom marié
`RE_LABEL_PRENOM` capture :
- Prénom : / Prénoms : / Prénom de naissance / utilisé(e) / usuel
- Capture jusqu'à fin de ligne pour les énumérations virgulées
(Prénoms : Sabine, Marie → tout masqué).
`RE_LABEL_VILLE` capture :
- Ville : / Ville de résidence : / Ville de naissance :
- Capture jusqu'à fin de ligne (gère "Saint-Jean-de-Luz",
"Saint-Denis (974)", composés multi-tokens).
Effets de bord positifs :
- Le bug "Saint-Jean-de-Luz → [ETABLISSEMENT]-de-Luz" est corrigé :
le matcher `RE_LABEL_VILLE` masque toute la valeur en `[VILLE]`
AVANT que le gazetteer FINESS Aho-Corasick ne grignote "Saint-Jean".
Cas 006_trackare_soignants et 008_anesthesie_complete : alignement
des expected.txt sur cette amélioration.
Choix d'architecture (cf cadrage docs/cadrage-projet-anonymisation.md
section 10.1) : ces labels sont des règles cœur génériques applicables
à tout établissement de santé français. Légitimes en hardcodé. Les
patterns layout-specific (Bordeaux suffixe, CHCB en fin de phrase,
email cassé par force_term) seront branchés via admin_rules dans
l'étape suivante.
Cas 010_fiche_admission_minimale passe désormais (retiré de
KNOWN_FAILURES). Le xfail strict aurait signalé xpass.
Tests : 9 passed, 2 xfailed (avant : 8 passed, 3 xfailed sur
test_synthetic_review).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trois fixes regroupés issus de la session de revue couche 2 :
#6 — caractère ñ dans les patterns de noms
Étend les classes de caractères pour inclure Ñ/ñ (basque, hispanique).
Avant : `Beñat` → `[NOM]ñat` (fuite indirecte du suffixe).
Après : `Beñat` → `[NOM]` (capture complète).
Justification : usage prévu La Réunion + populations basques/
hispaniques. Si nécessaire on ajoutera Ã/ã, Õ/õ (portugais) plus
tard.
#10 — règle numéro adhérent mutuelle (nouveau)
Ajoute placeholder [ADHERENT] et `RE_NUM_ADHERENT` :
`(?:n[°o]?\s*|num[ée]ro\s+(?:d['’]\s*)?)adh[ée]rent[e]?\s*[:\-]?\s*([A-Z0-9]{6,15})`
Couvre `n°adhérent`, `n° adhérent:`, `Numéro d'adhérent :`,
`Numéro d'adhérente:`, `numero adherent`, alphanumérique 6-15.
Faux positif `Le patient est adhérent à la mutuelle.` non matché
(préfixe N°/numéro obligatoire).
Branché dans `_mask_structured_line` (pour conserver le préfixe
au moment du matching, avant le split key:value) et dans
`_mask_line_by_regex` (texte non-structuré).
#11 — NIR avant TEL pour éviter consommation prématurée
Réordonne RE_NIR avant RE_TEL dans `_mask_line_by_regex` et
`selective_rescan`. Le NIR au format espacé `2 73 04 65 100 100 88`
est testé d'abord (validation modulo 97). Si validé, masqué en
[NIR] avant que RE_TEL ne consomme les 10 chiffres centraux. Si
la clé échoue (faux positif), TEL reprend la main inchangé.
Avant : `2 73 04 65 100 100 68` → `2 73 [TEL] 68`.
Après : `2 73 04 65 100 100 68` → `[NIR]`.
Cas synthetic_review/010 corrigé : NIR de test mis à clé valide
(68 au lieu de 88), expected aligné sur [ADHERENT] et [NIR].
Le case 010 reste en xfail — fuites résiduelles ELIZONDO / Sabine
/ Bayonne (labels structurels Nom de jeune fille / Prénom / Ville
non couverts) à fixer dans le batch suivant.
Tests : 70 passed, 3 xfailed (inchangé). Pas de régression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Couche 2 (revue humaine sur documents complets) : ajout de 6 cas
synthétiques pour atteindre la cible cadrage produit (10 cas).
Cas ajoutés :
- 005_bacterio_complete : layout BACTERIO N° venue rejeté avant IPP
+ RPPS prescripteur (pattern qualifié non détecté).
- 006_trackare_soignants : export Trackare avec activités HH:MM NOM,
Note IDE/médicale, Signé — médicament greedy.
- 007_lettre_sortie_complete : courrier médecin→médecin, multi-villes,
email institutionnel @chcb.fr (cassé par le force_term CHCB).
- 008_anesthesie_complete : protocole anesthésique avec molécules
BDPM, prénoms basques rares (Maddi, Pantxoa).
- 009_multi_etablissements : 3 établissements distincts (CHCB, CHU
Bordeaux, Clinique Aguilera), prénoms basques avec ñ (Beñat).
- 010_fiche_admission_minimale : fiche administrative dense, labels
variés (Nom de jeune fille :, Prénom :, Ville :, Mutuelle :).
Gate pytest (tests/unit/test_synthetic_review.py) :
- vérifie l'inventaire (10 cas) et fait passer chaque cas via run_case.
- 3 cas marqués xfail(strict=True) pour révéler 9 fuites de PII et
2 patterns partiels que le moteur ne couvre pas aujourd'hui :
* 005 — RPPS avec qualificateur (RPPS prescripteur :)
* 009 — Bordeaux résiduel après [ETAB], CHCB en fin de phrase,
Biarritz sur ligne Ville :, ñ qui casse Beñat → [NOM]ñat
* 010 — Nom de jeune fille / Prénom / Ville sans label "Patient :",
NIR au format espacé partiellement consommé en TEL,
numéro de mutuelle MGEN non couvert
- xfail strict force pytest à signaler un xpass quand un fix passe :
rappel automatique de retirer l'entrée de KNOWN_FAILURES.
Le runner tools/run_synthetic_review_corpus.py reste utilisable en
direct (sortie diff/audit/summary) pour la revue humaine. Les sorties
actual/ sont gitignorées (régénérées à chaque exécution).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L'OCR est désormais une vraie dépendance et plus une option commentée :
chaque page pauvre en texte natif doit pouvoir basculer sur docTR sans
avoir à demander une installation manuelle. Cohérent avec la priorité
qualité maximale sur la détection PII.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- RE_SCAN_FILENAME_ARTIFACT : masque le suffixe numérique des noms de
fichiers internes type EXT2-[IPP]-2300249096.TIF qui fuyaient en sortie.
- _RE_VENUE_BEFORE_IPP : variante BACTERIO observée en production où
le N° venue est rejeté plusieurs lignes après le libellé, juste
avant IPP. Détection en phase 0i.
- _RE_FINAL_VENUE_BEFORE_IPP : nettoyage final pour le résiduel du
même layout BACTERIO si le numéro a survécu jusqu'à process_pdf.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ma précédente modif affichait les étapes dans un SECOND splash tkinter
qui s'ouvrait après le splash natif PyInstaller. L'utilisateur voulait
voir les étapes dans la PREMIÈRE fenêtre (splash natif avec logo).
Refonte launch_gui() :
- Suppression du splash tkinter intermédiaire (pas de fenêtre qui clignote)
- Le splash natif PyInstaller reste visible pendant toute la phase d'import
- Handler logging installé sur le root logger pour intercepter chaque
log.info() du core. Traduction en libellé lisible + pyi_splash.update_text()
- Import synchrone (pas besoin de thread puisque le splash natif tourne
dans son propre processus bootloader)
- À la fin : splash natif fermé + lancement de la GUI principale
Résultat : l'utilisateur voit une seule fenêtre (splash natif avec logo)
où défilent sous le message "Démarrage…" toutes les étapes de chargement
des gazetteers, modèles et index. Quand tout est prêt, le splash disparaît
et la GUI apparaît. Plus de fenêtre intermédiaire.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Demande utilisateur : voir défiler les étapes (chargement des dictionnaires,
des modèles...) dans le splash au démarrage — effet pro apprécié des clients.
Implémentation :
- Nouveau handler logging.Handler installé sur le root logger avant l'import
du core. Intercepte chaque log.info() et :
* Traduit le message technique en libellé "prod" lisible (table de
correspondance _LOG_TRANSLATIONS : "Gazetteers INSEE prénoms" →
"Chargement des prénoms français (INSEE)…", etc.)
* Pousse le libellé dans le splash tkinter (detail_var, label secondaire)
* Pousse aussi dans le splash natif PyInstaller via pyi_splash.update_text()
- Splash tkinter agrandi 440×200 → 480×240 pour la nouvelle ligne détail
- Couleur primaire magenta (#E91E63) pour cohérence avec la GUI principale
- Handler retiré quand le splash se ferme (évite impact sur la GUI)
L'utilisateur voit maintenant défiler :
Chargement des prénoms français (INSEE)…
Chargement des noms de famille (INSEE)…
Chargement des communes françaises (INSEE)…
Chargement des numéros FINESS…
Indexation des établissements de santé…
Chargement du lexique médical…
Chargement de la base médicamenteuse (BDPM)…
Chargement des stop-words…
Chargement du vocabulaire clinique…
Chargement des phrases protégées…
Moteur d'anonymisation prêt…
Interface prête — finalisation…
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Intégration du logo "aivanonym" (gradient magenta → rose → pêche → noir)
fourni par le propriétaire. Refonte visuelle complète :
• APP_VERSION bump v5.4 → v5.5
• Assets (tous générés depuis assets/icons/logo.png) :
- assets/icons/app.ico multi-résolution 16→256 (icône EXE Windows)
- assets/icons/icon_{16,32,48,64,128,256,512}.png (fallback + taskbar)
- assets/logo_header.png (260×61, intégré dans l'en-tête de la GUI)
- assets/logo_splash.png (335×80, intégré dans le splash)
- assets/splash.png redessiné avec logo + bandeau gradient primary→accent
• Palette dérivée du logo (remplace l'ancien bleu) :
- CLR_PRIMARY #E91E63 magenta logo (CTA, liens)
- CLR_PRIMARY_DARK #C2185B hover / pressed
- CLR_PRIMARY_LIGHT #FCE4EC fond doux (tags, cartes)
- CLR_ACCENT #FFB74D pêche logo (secondaire)
- CLR_ACCENT_LIGHT #FFF3E0
- CLR_TEXT/SECONDARY proches du noir/gris du logo
• Pseudonymisation_Gui_V5.py :
- Helper _asset(name) : résout sous sys._MEIPASS/assets en mode frozen
- _apply_window_icon() : iconbitmap (.ico sur Windows) + iconphoto (PNG)
- _load_image_safe() : charge PIL avec ref persistante (évite GC tkinter)
- Header fixe hors onglets : logo image + baseline "100% local"
- Ligne accent magenta sous le header (inspiration logo)
- Onglets custom uniformes (remplace ttk.Notebook dont les tabs avaient
des tailles variables selon l'état) : tous les boutons identiques,
seule une bordure basse magenta signale l'onglet actif. _switch_tab()
gère l'affichage du contenu et la mise à jour des styles.
- Onglet 1 "Anonymisation" : workflow principal (choix, lancer, résultats)
- Onglet 2 "Paramètres" : 3 listes (whitelist/blacklist/stopwords) +
export/import + save. Plus de section repliable — respiration visuelle.
- Boutons export/import repensés avec les couleurs de la palette
• anonymisation_onefile.spec :
- datas : ajout du dossier assets/ entier
- EXE(icon=assets/icons/app.ico) : le .exe a maintenant le logo dans
l'Explorateur Windows, la barre des tâches, le gestionnaire des tâches
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Demande utilisateur : pouvoir identifier la build au premier coup d'oeil
sans confondre ancien/nouveau exe lors des tests.
Implémentation :
- build_info.py (gitignored, fallback "dev" pour mode développement)
régénéré automatiquement par scripts/rebuild_anon.ps1 avec :
BUILD_DATE = "2026-04-15 18:15"
BUILD_COMMIT = "7665ef1"
BUILD_BRANCH = "main"
- Pseudonymisation_Gui_V5.py : fonction _version_long() qui construit
"v5.4 · 2026-04-15 18:15 · #7665ef1" depuis build_info (avec fallback
silencieux si module absent en dev). Affichée dans :
- Titre fenêtre : "Pseudonymisation de vos documents — v5.4 · ..."
- Status bar en bas à droite
- anonymisation_onefile.spec : build_info.py ajouté aux datas bundlées.
- scripts/rebuild_anon.ps1 : STEP 4a génère build_info.py avant le
PyInstaller avec git rev-parse short + branch + date courante.
- .gitignore : build_info.py exclu (volatile, regénéré).
En mode dev (pas frozen) : affichage "v5.4" seul (fallback).
En mode frozen : affichage complet avec date/commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Message cosmétique sur Windows : "Prêt (NER indisponible : optimum.onnxruntime
introuvable. Installez 'optimum' et 'onnxruntime')". Apparaît dans la barre de
statut de la GUI quand EDS-Pseudo échoue à charger, et que le fallback
ner_manager_onnx.py essaie d'utiliser optimum.
Cause : 'optimum' n'était pas dans hiddenimports → PyInstaller ne le bundlait
pas → ner_manager_onnx.py mettait ORTModelForTokenClassification = None au
niveau module → l'appel à load() levait RuntimeError.
Le pipeline principal (CamemBERT-bio ONNX + EDS-Pseudo + GLiNER) ne passe
PAS par ner_manager_onnx.py — il utilise camembert_ner_manager.py qui charge
directement l'ONNX via onnxruntime sans optimum. Donc le masquage fonctionne
correctement malgré ce message. Mais le message inquiète l'utilisateur.
Fix : ajouter optimum + sous-modules aux hiddenimports. Impact taille
attendu : ~30-80 MB selon les dépendances embarquées.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Après deux rebuilds Windows silencieusement échoués (PermissionError
WinError 5 lors du os.remove par PyInstaller), amélioration du script :
1. Renommer l'ancien Anonymisation.exe en Anonymisation.old-HHMMSS.exe
AVANT le build (au lieu de laisser PyInstaller faire os.remove qui
échoue si Defender tient un handle). Move-Item bypass la plupart des
scanners antivirus.
2. Exclusions Defender sur dist/ et build/ (Add-MpPreference).
3. Retry Remove-Item avec délai 10s × 5 sur build/ en cas de lock.
4. Vérification timestamp APRÈS/AVANT : si l'exe final a le même
LastWriteTime qu'avant le build, exit code 2 "ÉCHEC CRITIQUE —
timestamp inchangé". Évite le faux OK quand le build rate mais que
l'ancien exe subsiste.
5. Encodage UTF-8 BOM nécessaire pour PowerShell Windows (accents
français dans les messages).
Validé : rebuild v5d a passé — nouveau exe 17:47:40 (vs ancien 17:09:32),
ancien renommé en Anonymisation.old-174023.exe.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Audit manuel après batch QC : 20 occurrences de "Dr Ute" dans
trackare-03020576-23175616 non masquées. Audit jsonl confirme : 0 hit pour
"Ute" → pas détecté.
Cause : _add_candidate (deux implémentations, lignes 1908 et 2225) filtrait
len(token) < 4, empêchant la création du NameCandidate pour "Ute" (3 chars)
même avec bypass_stopwords=True. La cross-validation écrasait alors
all_names avec validated_names (vide pour Ute), et _apply_extracted_names
ne recevait donc jamais Ute.
Le commit 2f79f7c avait fait le fix uniquement dans _apply_extracted_names.
Fix incomplet : le filtre amont _add_candidate rejetait avant.
Correctif symétrique sur _add_candidate (×2) + _add_tokens_force_first :
accepter 3 chars UNIQUEMENT si bypass=True (contexte Dr/Mme) ET majuscule
initiale ET alpha pur. 2 chars reste filtré (initiales ambigues).
Validation :
- "DR. DURANTEAU Ute" matche bien RE_EXTRACT_DR_DEST et capture "DURANTEAU Ute"
- Audit produit "Ute DURANTEAU" en bloc + "DURANTEAU" seul (41 hits total)
- PDF redacted : 0 résiduel "Ute" (avant : 38)
Cas protégés :
- "Ute" accepté : bypass=True, U majuscule, alpha ✓
- "Les" refusé : bypass=True mais stopword (filtré ailleurs) ✓
- "JF" refusé : 2 chars, filtre longueur < 3 ✓
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
L'utilisateur a signalé un chevauchement visuel entre la ligne statique
"Premier lancement : 30-60 secondes…" du PNG et la ligne dynamique
PyInstaller (qui affiche "Chargement EDS-Pseudo…", etc.) affichée par
pyi_splash.update_text().
Correctifs :
- PNG redessiné avec 3 lignes statiques seulement (titre, sous-titre,
"Démarrage en cours — merci de patienter…") et une ZONE LIBRE y=170-235
pour le texte dynamique.
- text_pos du Splash() ajusté à (60, 195) pour centrer dans la zone libre.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
L'exe --onefile décompresse ~720 Mo dans %TEMP% au lancement. Sur Windows,
cela prend 15-30 s AVANT que Python ne démarre. Pendant ce temps :
- Aucune fenêtre visible (même le splash tkinter existant n'était pas encore
exécuté, car il faut d'abord l'import de Python).
- L'utilisateur clique parfois plusieurs fois, croit que l'app est plantée.
Solution : Splash natif PyInstaller (Splash() dans le .spec). L'image est
affichée PAR LE BOOTLOADER de l'exe, AVANT même le démarrage Python. Le
texte sous l'image est actualisable via pyi_splash.update_text(), puis
fermé via pyi_splash.close() une fois le splash tkinter visible.
Changements :
- assets/splash.png (480x240) : titre + sous-titre + indication de durée
- anonymisation_onefile.spec : Splash() + splash/splash.binaries dans EXE()
- launcher.py : import pyi_splash (fallback silencieux en mode dev), helpers
_splash_update / _splash_close, fermeture du splash natif dès que le
splash tkinter est à l'écran (évite superposition).
- .gitignore : exception !assets/** pour versionner l'image du splash
(règle générale *.png exclut tout le reste).
Effet utilisateur attendu : fenêtre visible IMMÉDIATEMENT au double-clic,
avec message "Démarrage en cours — merci de patienter…". Suppression du
trou noir de 15-30 s.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>