Amélioration majeure de l'anonymisation regex : trackare, noms composés, faux positifs

- Parseur trackare spécifique (détection par contenu, extraction structurée des PII)
- Support format "Dr X. NOM" et "Mme X. NOM" (initiales + noms composés avec tiret)
- Détection noms personnel médical (Aide, Cadre Infirmier, etc.)
- Masquage RPPS, établissements (EHPAD/SSR/USLD standalone), lieux de naissance
- Stop words médicaux enrichis (~270 entrées : DCI, spécialités, termes contextuels)
- Détection compagnon (noms adjacents à des noms connus dans le texte brut)
- Protection noms composés (JEAN-PIERRE traité comme un tout, pas JEAN + PIERRE)
- Nettoyage codes postaux orphelins, téléphones fragmentés/partiels
- Désactivation masquage dates génériques, AGE avec contexte obligatoire
- GUI : extraction OGC depuis le nom du répertoire parent, incrustation sur les pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 00:25:18 +01:00
parent db1508d1c2
commit 5ed2312d93
2 changed files with 679 additions and 43 deletions

View File

@@ -399,7 +399,7 @@ class App:
self._folder_text_lbl = tk.Label(
self._folder_inner,
text="Cliquez pour choisir un dossier contenant vos PDF",
text="Cliquez pour choisir un dossier (tous les PDF seront recherchés récursivement)",
font=self._f_body, bg=CLR_CARD_BG, fg=CLR_TEXT_SECONDARY,
)
self._folder_text_lbl.pack(pady=(4, 0))
@@ -427,14 +427,15 @@ class App:
tk.Label(
info_inner,
text="Les deux formats sont générés automatiquement :",
text="Paramètres de traitement :",
font=self._f_body_bold, bg=CLR_BLUE_LIGHT, fg=CLR_TEXT, anchor="w",
).pack(fill=tk.X)
tk.Label(
info_inner,
text=("\u2022 PDF Image — sécurité maximale, chaque page en image, aucun texte résiduel\n"
"\u2022 PDF Anonymisé — structure préservée comme l'original, fichier léger"),
text=("\u2022 Recherche récursive de tous les PDF dans les sous-dossiers\n"
"\u2022 Sortie PDF Image (raster) — sécurité maximale, aucun texte résiduel\n"
"\u2022 Résultats dans le dossier « anonymise/ » à la racine"),
font=self._f_card_desc, bg=CLR_BLUE_LIGHT, fg=CLR_TEXT_SECONDARY,
anchor="w", justify=tk.LEFT,
).pack(fill=tk.X, pady=(4, 0))
@@ -589,10 +590,10 @@ class App:
if not folder:
return
# Compter les PDF
# Compter les PDF (récursif)
pdf_count = 0
try:
pdf_count = len([p for p in Path(folder).glob("*.pdf") if p.is_file()])
pdf_count = len([p for p in Path(folder).rglob("*.pdf") if p.is_file()])
except Exception:
pass
@@ -620,7 +621,7 @@ class App:
bg=CLR_CARD_BG, fg=CLR_TEXT, anchor="w",
).pack(fill=tk.X)
suffix = "PDF trouvé" if pdf_count <= 1 else "PDF trouvés"
suffix = "PDF trouvé (récursif)" if pdf_count <= 1 else "PDF trouvés (récursif)"
tk.Label(
info_frame, text=f"{pdf_count} {suffix}",
font=self._f_small, bg=CLR_CARD_BG, fg=CLR_TEXT_SECONDARY, anchor="w",
@@ -648,11 +649,11 @@ class App:
)
return
pdfs = sorted([p for p in folder.glob("*.pdf") if p.is_file()])
pdfs = sorted([p for p in folder.rglob("*.pdf") if p.is_file()])
if not pdfs:
messagebox.showwarning(
"Aucun PDF",
"Le dossier sélectionné ne contient aucun fichier PDF.",
"Aucun fichier PDF trouvé\n(recherche récursive dans les sous-dossiers).",
)
return
@@ -663,7 +664,7 @@ class App:
def _worker(self, folder: Path, pdfs: List[Path]):
try:
outdir = folder / "pseudonymise"
outdir = folder / "anonymise"
outdir.mkdir(exist_ok=True)
ok = ko = 0
global_counts: Dict[str, int] = {}
@@ -681,15 +682,21 @@ class App:
if use_ner and NerThresholds and not (EdsPseudoManager and isinstance(active, EdsPseudoManager)):
thresholds = NerThresholds(self.th_per, self.th_org, self.th_loc, 0.85)
# Extraire le numéro OGC du nom du répertoire parent
# Ex: "257_23209962" → OGC = "257"
parent_name = pdf.parent.name
ogc = parent_name.split("_")[0] if "_" in parent_name else None
outputs = core.process_pdf(
pdf_path=pdf,
out_dir=outdir,
make_vector_redaction=True,
make_vector_redaction=False,
also_make_raster_burn=True,
config_path=Path(self.cfg_path.get()),
use_hf=use_ner,
ner_manager=active,
ner_thresholds=thresholds,
ogc_label=ogc,
)
self.queue.put(UiMessage(kind=MsgType.LOG, text=f"\u2713 {pdf.name}"))
for k, v in outputs.items():
@@ -822,15 +829,15 @@ class App:
def _show_help(self):
messagebox.showinfo(
"Comment ça marche ?",
"1) Choisissez le dossier contenant vos fichiers PDF.\n\n"
"1) Choisissez le dossier racine contenant vos fichiers PDF.\n\n"
"2) Cliquez sur « Lancer la pseudonymisation ».\n\n"
"Deux fichiers sont générés pour chaque PDF :\n"
" \u2022 PDF Image : chaque page devient une image avec les\n"
" données masquées. Sécurité maximale.\n"
" \u2022 PDF Anonymisé : structure préservée comme l'original,\n"
" fichier léger et texte sélectionnable.\n\n"
"Les résultats apparaissent dans un sous-dossier\n"
"« pseudonymise » à côté de vos originaux.",
"Tous les fichiers PDF sont traités\n"
"(recherche récursive dans les sous-dossiers).\n\n"
"Un PDF Image (raster) est généré pour chaque fichier :\n"
"chaque page devient une image avec les données masquées.\n"
"Sécurité maximale, aucun texte résiduel.\n\n"
"Les résultats sont regroupés à plat dans le dossier\n"
"« anonymise/ » à la racine du dossier sélectionné.",
)
# ---------------------------------------------------------------