feat: whitelist phrases + panneau paramètres avancés dans la GUI
- Nouvelle section whitelist_phrases dans dictionnaires.yml : phrases qui ne doivent jamais être anonymisées (FP récurrents) - Fonction _apply_whitelist : restaure les phrases whitelistées après anonymisation, même si des mots ont été remplacés par des placeholders - GUI : section "Paramètres avancés" repliable avec : - Zone texte whitelist (phrases à exclure) - Zone texte blacklist (mots à toujours masquer) - Bouton sauvegarder → persiste dans le YAML - Phrases initiales : "classification internationale", "prise en charge", "bas de contention", "date de naissance", "code postal", etc. Score évaluation maintenu à 100.0/100 (A+) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -504,9 +504,72 @@ class App:
|
|||||||
main, text="Comment ça marche ?", font=self._f_small,
|
main, text="Comment ça marche ?", font=self._f_small,
|
||||||
bg=CLR_BG, fg=CLR_PRIMARY, cursor="hand2",
|
bg=CLR_BG, fg=CLR_PRIMARY, cursor="hand2",
|
||||||
)
|
)
|
||||||
help_lbl.pack(pady=(0, 18))
|
help_lbl.pack(pady=(0, 8))
|
||||||
help_lbl.bind("<Button-1>", lambda e: self._show_help())
|
help_lbl.bind("<Button-1>", lambda e: self._show_help())
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# SECTION PARAMÈTRES (repliable)
|
||||||
|
# =============================================================
|
||||||
|
self._params_visible = False
|
||||||
|
params_toggle = tk.Label(
|
||||||
|
main, text="\u2699 Paramètres avancés \u25B6", font=self._f_small,
|
||||||
|
bg=CLR_BG, fg=CLR_PRIMARY, cursor="hand2",
|
||||||
|
)
|
||||||
|
params_toggle.pack(pady=(0, 4), padx=pad_x, anchor="w")
|
||||||
|
|
||||||
|
self._params_frame = tk.Frame(main, bg=CLR_BG)
|
||||||
|
# NE PAS pack — déplié à la demande
|
||||||
|
|
||||||
|
def _toggle_params(event=None):
|
||||||
|
if self._params_visible:
|
||||||
|
self._params_frame.pack_forget()
|
||||||
|
params_toggle.configure(text="\u2699 Paramètres avancés \u25B6")
|
||||||
|
else:
|
||||||
|
self._params_frame.pack(fill=tk.X, padx=pad_x, pady=(0, 12))
|
||||||
|
params_toggle.configure(text="\u2699 Paramètres avancés \u25BC")
|
||||||
|
self._params_visible = not self._params_visible
|
||||||
|
params_toggle.bind("<Button-1>", _toggle_params)
|
||||||
|
|
||||||
|
# --- Whitelist (phrases à ne pas anonymiser) ---
|
||||||
|
tk.Label(
|
||||||
|
self._params_frame,
|
||||||
|
text="Phrases à ne PAS anonymiser (une par ligne) :",
|
||||||
|
font=self._f_small, bg=CLR_BG, fg=CLR_TEXT, anchor="w",
|
||||||
|
).pack(fill=tk.X, pady=(4, 2))
|
||||||
|
|
||||||
|
self._whitelist_text = tk.Text(
|
||||||
|
self._params_frame, height=5, font=("Consolas", 9),
|
||||||
|
wrap=tk.WORD, relief=tk.GROOVE, bd=1,
|
||||||
|
)
|
||||||
|
self._whitelist_text.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
|
||||||
|
# --- Blacklist (phrases à toujours masquer) ---
|
||||||
|
tk.Label(
|
||||||
|
self._params_frame,
|
||||||
|
text="Mots/phrases à TOUJOURS masquer (une par ligne) :",
|
||||||
|
font=self._f_small, bg=CLR_BG, fg=CLR_TEXT, anchor="w",
|
||||||
|
).pack(fill=tk.X, pady=(0, 2))
|
||||||
|
|
||||||
|
self._blacklist_text = tk.Text(
|
||||||
|
self._params_frame, height=5, font=("Consolas", 9),
|
||||||
|
wrap=tk.WORD, relief=tk.GROOVE, bd=1,
|
||||||
|
)
|
||||||
|
self._blacklist_text.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
|
||||||
|
# Bouton sauvegarder
|
||||||
|
save_btn = tk.Button(
|
||||||
|
self._params_frame, text="Sauvegarder les paramètres",
|
||||||
|
font=self._f_small, bg=CLR_CARD_BG, fg=CLR_TEXT,
|
||||||
|
relief=tk.GROOVE, cursor="hand2",
|
||||||
|
command=self._save_params,
|
||||||
|
)
|
||||||
|
save_btn.pack(anchor="e", pady=(0, 4))
|
||||||
|
|
||||||
|
# Charger les valeurs initiales depuis la config
|
||||||
|
self._load_params()
|
||||||
|
|
||||||
|
ttk.Separator(main).pack(fill=tk.X, padx=pad_x, pady=(0, 8))
|
||||||
|
|
||||||
# =============================================================
|
# =============================================================
|
||||||
# BARRE DE PROGRESSION (masquée)
|
# BARRE DE PROGRESSION (masquée)
|
||||||
# =============================================================
|
# =============================================================
|
||||||
@@ -1042,6 +1105,58 @@ class App:
|
|||||||
"« anonymise/ » à la racine du dossier sélectionné.",
|
"« anonymise/ » à la racine du dossier sélectionné.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Paramètres avancés (whitelist/blacklist)
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
def _load_params(self):
|
||||||
|
"""Charge les whitelist/blacklist depuis la config YAML."""
|
||||||
|
try:
|
||||||
|
cfg_path = Path(self.cfg_path.get())
|
||||||
|
if cfg_path.exists() and yaml is not None:
|
||||||
|
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
|
||||||
|
# Whitelist
|
||||||
|
wl = data.get("whitelist_phrases", [])
|
||||||
|
if wl:
|
||||||
|
self._whitelist_text.delete("1.0", tk.END)
|
||||||
|
self._whitelist_text.insert("1.0", "\n".join(wl))
|
||||||
|
# Blacklist
|
||||||
|
bl = data.get("blacklist", {}).get("force_mask_terms", [])
|
||||||
|
if bl:
|
||||||
|
self._blacklist_text.delete("1.0", tk.END)
|
||||||
|
self._blacklist_text.insert("1.0", "\n".join(str(t) for t in bl))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_params(self):
|
||||||
|
"""Sauvegarde les whitelist/blacklist dans la config YAML."""
|
||||||
|
try:
|
||||||
|
cfg_path = Path(self.cfg_path.get())
|
||||||
|
if not cfg_path.exists() or yaml is None:
|
||||||
|
messagebox.showwarning("Erreur", "Fichier de configuration introuvable.")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
|
||||||
|
|
||||||
|
# Whitelist phrases
|
||||||
|
wl_text = self._whitelist_text.get("1.0", tk.END).strip()
|
||||||
|
wl_lines = [l.strip() for l in wl_text.split("\n") if l.strip()]
|
||||||
|
data["whitelist_phrases"] = wl_lines
|
||||||
|
|
||||||
|
# Blacklist terms
|
||||||
|
bl_text = self._blacklist_text.get("1.0", tk.END).strip()
|
||||||
|
bl_lines = [l.strip() for l in bl_text.split("\n") if l.strip()]
|
||||||
|
if "blacklist" not in data:
|
||||||
|
data["blacklist"] = {}
|
||||||
|
data["blacklist"]["force_mask_terms"] = bl_lines
|
||||||
|
|
||||||
|
cfg_path.write_text(
|
||||||
|
yaml.dump(data, allow_unicode=True, default_flow_style=False, sort_keys=False),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
messagebox.showinfo("Paramètres", "Paramètres sauvegardés avec succès.")
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Erreur", f"Impossible de sauvegarder :\n{e}")
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
# YAML (interne)
|
# YAML (interne)
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
|
|||||||
@@ -3106,6 +3106,49 @@ def _mask_ville_gazetteers(text: str) -> tuple:
|
|||||||
return "".join(result), masked_originals
|
return "".join(result), masked_originals
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------- Whitelist (phrases à ne jamais anonymiser) -----------------
|
||||||
|
|
||||||
|
def _apply_whitelist(text: str, phrases: List[str], audit: List[PiiHit]) -> str:
|
||||||
|
"""Restaure les phrases whitelistées qui ont été masquées à tort.
|
||||||
|
|
||||||
|
Pour chaque phrase de la whitelist, construit un pattern flexible qui
|
||||||
|
accepte des placeholders [XXX] entre les mots originaux.
|
||||||
|
Ex: "bas de contention" matche "bas [NOM] contention" ou "bas de [NOM]".
|
||||||
|
"""
|
||||||
|
_PH = r"\[[A-Z_]+\]" # placeholder pattern
|
||||||
|
|
||||||
|
for phrase in phrases:
|
||||||
|
if not phrase or not phrase.strip():
|
||||||
|
continue
|
||||||
|
words = phrase.strip().split()
|
||||||
|
if len(words) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Construire un pattern où chaque mot de la phrase peut être
|
||||||
|
# remplacé par un placeholder OU être présent tel quel.
|
||||||
|
# Entre les mots : espace(s) optionnel(s)
|
||||||
|
parts = []
|
||||||
|
for w in words:
|
||||||
|
# Le mot original OU un placeholder
|
||||||
|
parts.append(rf"(?:{re.escape(w)}|{_PH})")
|
||||||
|
# Joindre avec des espaces flexibles
|
||||||
|
pattern = r"(?i)" + r"[\s]+".join(parts)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rx = re.compile(pattern)
|
||||||
|
except re.error:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for m in rx.finditer(text):
|
||||||
|
matched = m.group(0)
|
||||||
|
# Ne restaurer que si au moins un placeholder est présent
|
||||||
|
# (sinon la phrase est déjà intacte, pas besoin de toucher)
|
||||||
|
if "[" in matched:
|
||||||
|
text = text[:m.start()] + phrase + text[m.end():]
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
# ----------------- Selective safety rescan -----------------
|
# ----------------- Selective safety rescan -----------------
|
||||||
|
|
||||||
def selective_rescan(text: str, cfg: Dict[str, Any] | None = None) -> str:
|
def selective_rescan(text: str, cfg: Dict[str, Any] | None = None) -> str:
|
||||||
@@ -4038,6 +4081,11 @@ def process_pdf(
|
|||||||
)
|
)
|
||||||
final_text = _RE_BRACKET_CLEAN.sub(r"\1", final_text)
|
final_text = _RE_BRACKET_CLEAN.sub(r"\1", final_text)
|
||||||
|
|
||||||
|
# 6) Whitelist : restaurer les phrases qui ne doivent jamais être anonymisées
|
||||||
|
whitelist_phrases = cfg.get("whitelist_phrases", [])
|
||||||
|
if whitelist_phrases:
|
||||||
|
final_text = _apply_whitelist(final_text, whitelist_phrases, anon.audit)
|
||||||
|
|
||||||
# Sauvegardes
|
# Sauvegardes
|
||||||
base = pdf_path.stem
|
base = pdf_path.stem
|
||||||
txt_path = out_dir / f"{base}.pseudonymise.txt"
|
txt_path = out_dir / f"{base}.pseudonymise.txt"
|
||||||
|
|||||||
@@ -47,6 +47,19 @@ regex_overrides:
|
|||||||
placeholder: '[OGC]'
|
placeholder: '[OGC]'
|
||||||
flags:
|
flags:
|
||||||
- IGNORECASE
|
- IGNORECASE
|
||||||
|
# Phrases à ne JAMAIS anonymiser (faux positifs récurrents)
|
||||||
|
# Ajouter ici les expressions qui sont masquées à tort.
|
||||||
|
# La correspondance est insensible à la casse.
|
||||||
|
whitelist_phrases:
|
||||||
|
- "classification internationale"
|
||||||
|
- "prise en charge"
|
||||||
|
- "bas de contention"
|
||||||
|
- "date de naissance"
|
||||||
|
- "lieu de naissance"
|
||||||
|
- "ville de résidence"
|
||||||
|
- "date de sortie"
|
||||||
|
- "date d'admission"
|
||||||
|
- "code postal"
|
||||||
flags:
|
flags:
|
||||||
case_insensitive: true
|
case_insensitive: true
|
||||||
unicode_word_boundaries: true
|
unicode_word_boundaries: true
|
||||||
|
|||||||
Reference in New Issue
Block a user