Files
anonymisation/gui_v6/tabs/tab_config.py

420 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Onglet « Configuration » de la GUI V6 (G4 — alignement maquette).
Sous-navigation Réglages / Masquage / Partage / Règles (cf. maquette). Le
sous-onglet Réglages édite un :class:`ConfigState` partagé (profil, NER local,
moteurs). Les autres sous-onglets reprennent le style maquette (cartes denses).
Aucune logique de détection ; ``config_defaults.py`` n'est pas modifié.
"""
from __future__ import annotations
from pathlib import Path
from tkinter import filedialog
import customtkinter as ctk
from gui_v6 import theme as theme_mod
from gui_v6 import ui_kit
from gui_v6.config_state import ConfigState, default_profile_key, list_profile_keys
_SUBTABS = [
("reg", "⚙️ Réglages"),
("msk", "🎭 Masquage"),
("shr", "🔄 Partage"),
("rul", "🛡️ Règles 2"),
]
_DETECTION_OPTIONS = [
("Noms et prénoms", "Gazetteers INSEE · CamemBERT"),
("Dates de naissance", "Uniquement la date de naissance"),
("Établissements", "Répertoire FINESS + contexte"),
("Adresses et codes postaux", ""),
("N° sécurité sociale", ""),
("Téléphones et e-mails", ""),
("N° adhérent mutuelle", ""),
]
_PRESERVE_TERMS = ["FUROSEMIDE", "rééducation fonctionnelle", "classification internationale"]
_MASK_TERMS = ["CHUXX"]
CONFIG_MOCKUP_SECTIONS = {
"reglages": [
"Profil métier",
"Moteurs NER",
"Données à détecter",
"Termes à toujours conserver",
"Termes à toujours masquer",
],
"masquage": [
"Couleur de masquage (PDF)",
"Style des marqueurs (texte)",
"Épaisseur du masque",
"Codes de remplacement",
"Masques de zones fixes",
],
"partage": ["Exporter la configuration", "Importer une configuration"],
"regles": ["Règles actives", "Testeur de règle"],
}
class ConfigTab(ctk.CTkFrame):
def __init__(self, master, state: ConfigState | None = None, palette: dict | None = None, **kwargs):
self._p = palette or theme_mod.get_palette(theme_mod.DEFAULT_THEME)
super().__init__(master, fg_color=self._p["bg"], **kwargs)
self._state = state if state is not None else ConfigState()
self._sub = "reg"
self._sub_buttons: dict = {}
self._build()
@property
def state(self) -> ConfigState:
return self._state
def _build(self) -> None:
p = self._p
bar = ctk.CTkFrame(self, fg_color="transparent")
bar.pack(fill="x", padx=14, pady=(14, 4))
for key, label in _SUBTABS:
btn = ctk.CTkButton(
bar, text=label, command=lambda k=key: self._show_sub(k),
fg_color="transparent", hover_color=p["card_border"],
text_color=p["primary"] if key == self._sub else p["text_dim"],
font=ui_kit.font(13, "bold" if key == self._sub else "normal"),
corner_radius=0, width=10,
)
btn.pack(side="left", padx=3)
self._sub_buttons[key] = btn
self._body = ctk.CTkFrame(self, fg_color="transparent")
self._body.pack(fill="both", expand=True)
self._show_sub("reg")
def _show_sub(self, key: str) -> None:
self._sub = key
p = self._p
for k, btn in self._sub_buttons.items():
btn.configure(text_color=p["primary"] if k == key else p["text_dim"],
font=ui_kit.font(13, "bold" if k == key else "normal"))
for w in self._body.winfo_children():
w.destroy()
{"reg": self._build_reglages, "msk": self._build_masquage,
"shr": self._build_partage, "rul": self._build_regles}[key]()
# -- Réglages (câblé ConfigState) -------------------------------------
def _build_reglages(self) -> None:
p = self._p
cols = self._two_columns(self._body)
left, right = cols
prof = ui_kit.Card(left, p, title="🗂️ Profil métier")
prof.pack(fill="x", pady=7)
row = ctk.CTkFrame(prof, fg_color="transparent")
row.pack(fill="x", padx=16, pady=(0, 10))
profiles = list_profile_keys()
current = self._state.profile or default_profile_key() or (profiles[0] if profiles else "")
self._state.profile = current or None
ctk.CTkLabel(row, text="Profil :", text_color=p["text"], font=ui_kit.font(13)).pack(side="left", padx=(0, 8))
menu = ctk.CTkOptionMenu(row, values=profiles or ["(aucun profil)"], command=self._on_profile,
fg_color=p["btn_sec_bg"], button_color=p["primary"], text_color=p["text"])
if current:
menu.set(current)
menu.pack(side="left")
outrow = ctk.CTkFrame(prof, fg_color="transparent")
outrow.pack(fill="x", padx=16, pady=(0, 14))
ui_kit.secondary_button(outrow, p, "📁 Dossier de sortie…", command=self._pick_output).pack(side="left", padx=(0, 8))
self._out_label = ctk.CTkLabel(outrow, text=str(self._state.output_dir or "(défaut anonymise/)"),
text_color=p["text_muted"], font=ui_kit.font(12))
self._out_label.pack(side="left")
ner = ui_kit.Card(left, p, title="🧠 Moteurs NER")
ner.pack(fill="x", pady=7)
self._tog_ner = ui_kit.ToggleRow(ner, p, "CamemBERT-bio ⚡", "~10 ms/doc · F1 = 0.963", value=self._state.use_local_ner, command=self._on_ner)
self._tog_ner.pack(fill="x", padx=16, pady=2)
self._tog_eds = ui_kit.ToggleRow(ner, p, "EDS-Pseudo PRÉCIS", "~200 ms/doc · médical français", value=self._state.enable_eds, command=self._on_eds)
self._tog_eds.pack(fill="x", padx=16, pady=2)
self._tog_gli = ui_kit.ToggleRow(ner, p, "GLiNER OPTIONNEL", "~95 ms/doc · vote croisé", value=self._state.enable_gliner, command=self._on_gliner)
self._tog_gli.pack(fill="x", padx=16, pady=(2, 14))
det = ui_kit.Card(right, p, title="🔍 Données à détecter")
det.pack(fill="x", pady=7)
for label, hint in _DETECTION_OPTIONS:
ui_kit.ToggleRow(det, p, label, hint, value=True).pack(fill="x", padx=16, pady=2)
ctk.CTkFrame(det, fg_color="transparent", height=8).pack()
keep = ui_kit.Card(right, p, title="✅ Termes à toujours conserver")
keep.pack(fill="x", pady=7)
self._note(keep, "Ces termes ne seront jamais masqués, même s'ils ressemblent à un nom propre.")
self._tag_editor(keep, "Ex : FUROSEMIDE…", _PRESERVE_TERMS, "keep")
mask = ui_kit.Card(right, p, title="🚫 Termes à toujours masquer")
mask.pack(fill="x", pady=7)
self._note(mask, "Ces termes seront toujours masqués, même sans contexte médical autour.")
self._tag_editor(mask, "Ex : CHUXX, Dr Dupont…", _MASK_TERMS, "mask")
def _build_masquage(self) -> None:
p = self._p
cols = self._two_columns(self._body)
left, right = cols
color = ui_kit.Card(left, p, title="⬛ Couleur de masquage (PDF)")
color.pack(fill="x", pady=7)
self._note(color, "Couleur des rectangles dans le PDF final.")
swatches = ctk.CTkFrame(color, fg_color="transparent")
swatches.pack(fill="x", padx=16, pady=(0, 14))
for color_value in ["#000000", "#1a1a2e", "#374151", "#92400e", "#1e3a5f"]:
ctk.CTkButton(
swatches,
text="",
width=30,
height=30,
fg_color=color_value,
hover_color=color_value,
border_color=p["primary"] if color_value == "#000000" else p["card_border"],
border_width=3 if color_value == "#000000" else 1,
corner_radius=6,
command=lambda: None,
).pack(side="left", padx=(0, 8))
style = ui_kit.Card(left, p, title="🏷️ Style des marqueurs (texte)")
style.pack(fill="x", pady=7)
self._mask_style_var = ctk.StringVar(value="brackets")
for label, value, preview in [
("Crochets", "brackets", "[NOM]"),
("Étoiles", "stars", "***"),
("Noirci", "blackout", "████"),
]:
row = ctk.CTkFrame(style, fg_color="transparent")
row.pack(fill="x", padx=16, pady=2)
ctk.CTkRadioButton(
row,
text=f"{label}{preview}",
variable=self._mask_style_var,
value=value,
command=self._update_mask_preview,
text_color=p["text"],
fg_color=p["primary"],
hover_color=p["primary_dim"],
font=ui_kit.font(13),
).pack(anchor="w")
self._mask_preview = ctk.CTkLabel(
style,
text="Patient [NOM], né le [DATE_NAISSANCE]",
text_color=p["text_dim"],
fg_color=p["divider"],
corner_radius=6,
font=ui_kit.font(13),
anchor="w",
)
self._mask_preview.pack(fill="x", padx=16, pady=(8, 14), ipady=8)
thick = ui_kit.Card(right, p, title="📐 Épaisseur du masque")
thick.pack(fill="x", pady=7)
self._note(thick, "Marge autour du texte masqué, en points.")
self._slider_row(thick, "Marge horizontale", 2)
self._slider_row(thick, "Marge verticale", 1)
ui_kit.ToggleRow(thick, p, "Coins arrondis", "", value=False).pack(fill="x", padx=16, pady=(2, 14))
codes = ui_kit.Card(right, p, title="🔒 Codes de remplacement")
codes.pack(fill="x", pady=7)
for k, v in [
("Nom/Prénom", "[NOM]"),
("Date naissance", "[DATE_NAISSANCE]"),
("Établissement", "[ETABLISSEMENT]"),
("Adresse", "[ADRESSE]"),
("Téléphone", "[TEL]"),
("N° sécu", "[NIR]"),
("IPP", "[IPP]"),
("Email", "[EMAIL]"),
]:
r = ctk.CTkFrame(codes, fg_color="transparent")
r.pack(fill="x", padx=16, pady=1)
ctk.CTkLabel(r, text=k, text_color=p["text_muted"], font=ui_kit.font(12)).pack(side="left")
ctk.CTkLabel(r, text=v, text_color=p["primary"], font=ui_kit.font(12, "bold")).pack(side="right")
ctk.CTkFrame(codes, fg_color="transparent", height=8).pack()
editor = ui_kit.Card(self._body, p, title="🏠 Masques de zones fixes (logos, en-têtes)")
editor.pack(fill="x", padx=14, pady=7)
top = ctk.CTkFrame(editor, fg_color="transparent")
top.pack(fill="x", padx=16, pady=(0, 8))
ctk.CTkLabel(
top,
text="Dessinez des rectangles sur un PDF modèle pour masquer systématiquement les logos, en-têtes ou zones fixes.",
text_color=p["text_dim"],
font=ui_kit.font(12),
justify="left",
wraplength=610,
).pack(side="left", fill="x", expand=True)
ui_kit.primary_button(top, p, "✏ Ouvrir l'éditeur de masques", command=lambda: None).pack(side="right", padx=(8, 0))
toolbar = ctk.CTkFrame(editor, fg_color=p["divider"], border_color=p["card_border"], border_width=1, corner_radius=8)
toolbar.pack(fill="x", padx=16, pady=(0, 10))
for label in ["📄 Ouvrir PDF…", "", "100%", "+", "💾 Sauver", "📁 Charger", "🗑 Effacer page"]:
ui_kit.secondary_button(toolbar, p, label, command=lambda: None).pack(side="left", padx=4, pady=8)
canvas = ctk.CTkFrame(editor, fg_color=p["divider"], border_color=p["card_border"], border_width=1, corner_radius=8, height=150)
canvas.pack(fill="x", padx=16, pady=(0, 12))
canvas.pack_propagate(False)
ctk.CTkLabel(
canvas,
text="📄\nOuvrez un PDF pour commencer à dessiner des zones de masquage.\nCliquez-glissez pour tracer un rectangle.",
text_color=p["text_muted"],
font=ui_kit.font(12),
justify="center",
).pack(expand=True)
def _build_partage(self) -> None:
p = self._p
export = ui_kit.Card(self._body, p, title="📤 Exporter la configuration")
export.pack(fill="x", padx=14, pady=7)
self._note(export, "Génère un fichier .json avec vos listes, à envoyer par e-mail à d'autres établissements.")
ui_kit.secondary_button(export, p, "⬇ Exporter (.json)", command=lambda: None).pack(anchor="w", padx=16, pady=(0, 14))
import_card = ui_kit.Card(self._body, p, title="📥 Importer une configuration")
import_card.pack(fill="x", padx=14, pady=7)
self._note(import_card, "Importez un fichier reçu. Vos réglages locaux ne seront pas supprimés.")
ui_kit.secondary_button(import_card, p, "⬆ Importer (.json)", command=lambda: None).pack(anchor="w", padx=16, pady=(0, 14))
def _build_regles(self) -> None:
p = self._p
card = ui_kit.Card(self._body, p, title="🛡️ Règles actives")
card.pack(fill="x", padx=14, pady=7)
self._note(card, "Ces règles adaptent le moteur à votre établissement. Chaque règle est validée avant activation.")
headers = ctk.CTkFrame(card, fg_color="transparent")
headers.pack(fill="x", padx=16, pady=(0, 4))
for text, width in [("Label", 190), ("Type", 80), ("Cible → Résultat", 210), ("Statut", 70), ("", 70)]:
ctk.CTkLabel(headers, text=text.upper(), width=width, anchor="w", text_color=p["text_muted"], font=ui_kit.font(10, "bold")).pack(side="left")
for row in [
("Masquer le sigle CHUXX", "exact", "CHUXX → [MASK]", "Actif"),
("Préserver “classification internationale”", "preserve", "conservé tel quel", "Actif"),
("Identifier N° 1234567", "norm-id", "N° 1234567 → [NDA]", "Candidat"),
]:
self._rule_row(card, row)
actions = ctk.CTkFrame(card, fg_color="transparent")
actions.pack(fill="x", padx=16, pady=(10, 14))
ui_kit.primary_button(actions, p, "+ Nouvelle règle", command=lambda: None).pack(side="left", padx=(0, 8))
ui_kit.secondary_button(actions, p, "🔄 Recharger", command=lambda: None).pack(side="left")
sim = ui_kit.Card(self._body, p, title="🧪 Testeur de règle")
sim.pack(fill="x", padx=14, pady=7)
ctk.CTkLabel(sim, text="Texte de test", text_color=p["text_muted"], font=ui_kit.font(12)).pack(anchor="w", padx=16)
txt = ctk.CTkTextbox(sim, height=78, fg_color=p["divider"], text_color=p["text"], border_color=p["card_border"], border_width=1)
txt.pack(fill="x", padx=16, pady=(5, 10))
txt.insert("1.0", "Compte rendu CHUXX, patient N° 1234567.")
btns = ctk.CTkFrame(sim, fg_color="transparent")
btns.pack(fill="x", padx=16, pady=(0, 14))
ui_kit.primary_button(btns, p, "▶ Tester", command=lambda: None).pack(side="left", padx=(0, 8))
ui_kit.secondary_button(btns, p, "✖ Fermer", command=lambda: None).pack(side="left")
# -- callbacks --------------------------------------------------------
def _on_profile(self, value: str) -> None:
self._state.profile = value
def _on_ner(self) -> None:
self._state.use_local_ner = self._tog_ner.get()
def _on_eds(self) -> None:
self._state.enable_eds = self._tog_eds.get()
def _on_gliner(self) -> None:
self._state.enable_gliner = self._tog_gli.get()
def _pick_output(self) -> None:
path = filedialog.askdirectory(title="Dossier de sortie")
if path:
self._state.output_dir = Path(path)
self._out_label.configure(text=str(self._state.output_dir))
# -- helpers UI -------------------------------------------------------
def _two_columns(self, parent) -> tuple[ctk.CTkFrame, ctk.CTkFrame]:
row = ctk.CTkFrame(parent, fg_color="transparent")
row.pack(fill="both", expand=True, padx=14)
left = ctk.CTkFrame(row, fg_color="transparent")
right = ctk.CTkFrame(row, fg_color="transparent")
left.pack(side="left", fill="both", expand=True, padx=(0, 7))
right.pack(side="left", fill="both", expand=True, padx=(7, 0))
return left, right
def _note(self, parent, text: str) -> None:
p = self._p
ctk.CTkLabel(
parent,
text=text,
text_color=p["text_muted"],
fg_color=p["divider"],
corner_radius=4,
font=ui_kit.font(11),
anchor="w",
justify="left",
wraplength=330,
).pack(fill="x", padx=16, pady=(0, 10), ipady=5)
def _tag_editor(self, parent, placeholder: str, terms: list[str], kind: str) -> None:
p = self._p
row = ctk.CTkFrame(parent, fg_color="transparent")
row.pack(fill="x", padx=16, pady=(0, 8))
entry = ctk.CTkEntry(
row,
placeholder_text=placeholder,
fg_color=p["btn_sec_bg"],
border_color=p["btn_sec_border"],
text_color=p["text"],
height=32,
)
entry.pack(side="left", fill="x", expand=True, padx=(0, 8))
ui_kit.secondary_button(row, p, "+ Ajouter", command=lambda: None).pack(side="right")
cloud = ctk.CTkFrame(parent, fg_color="transparent")
cloud.pack(fill="x", padx=16, pady=(0, 14))
tag_color = p["success"] if kind == "keep" else p["primary"]
line = ctk.CTkFrame(cloud, fg_color="transparent")
line.pack(fill="x", anchor="w")
used_width = 0
for term in terms:
estimated_width = min(230, max(88, len(term) * 8 + 34))
if used_width and used_width + estimated_width > 360:
line = ctk.CTkFrame(cloud, fg_color="transparent")
line.pack(fill="x", anchor="w")
used_width = 0
ctk.CTkLabel(
line,
text=f"{term[:30]}{'' if len(term) > 30 else ''} ×",
width=estimated_width,
text_color=tag_color,
fg_color=p["btn_sec_bg"],
corner_radius=99,
font=ui_kit.font(12),
).pack(side="left", padx=(0, 6), pady=3, ipadx=4, ipady=3)
used_width += estimated_width + 6
def _slider_row(self, parent, label: str, value: int) -> None:
p = self._p
row = ctk.CTkFrame(parent, fg_color="transparent")
row.pack(fill="x", padx=16, pady=3)
ctk.CTkLabel(row, text=label, text_color=p["text"], font=ui_kit.font(13)).pack(side="left")
slider = ctk.CTkSlider(row, from_=0, to=6, number_of_steps=6, progress_color=p["primary"], width=125)
slider.set(value)
slider.pack(side="right")
def _update_mask_preview(self) -> None:
value = self._mask_style_var.get()
if value == "stars":
text = "Patient ***, né le ***"
elif value == "blackout":
text = "Patient ████, né le ████"
else:
text = "Patient [NOM], né le [DATE_NAISSANCE]"
self._mask_preview.configure(text=text)
def _rule_row(self, parent, values: tuple[str, str, str, str]) -> None:
p = self._p
label, rule_type, target, status = values
row = ctk.CTkFrame(parent, fg_color="transparent")
row.pack(fill="x", padx=16, pady=1)
ctk.CTkLabel(row, text=label, width=190, anchor="w", text_color=p["text"], font=ui_kit.font(12)).pack(side="left")
ctk.CTkLabel(row, text=rule_type, width=80, anchor="w", text_color=p["text_dim"], font=ui_kit.font(11)).pack(side="left")
ctk.CTkLabel(row, text=target, width=210, anchor="w", text_color=p["primary"], font=ui_kit.font(12, "bold")).pack(side="left")
color = p["success"] if status == "Actif" else p["warning"]
ctk.CTkLabel(row, text=status, width=70, anchor="w", text_color=color, font=ui_kit.font(11, "bold")).pack(side="left")
ui_kit.secondary_button(row, p, "▶ Tester", command=lambda: None).pack(side="left")