Commit Graph

89 Commits

Author SHA1 Message Date
299bbee5ff fix(detect): F5 — masque la continuation orpheline d'un nom composé (EJNAINI)
Dernière fuite de l'audit_30. Cas Trackare : un nom composé "NOCENT-EJNAINI"
éclaté en colonnes devient "[NOM]-\nEJNAINI" — le 1er composant est masqué
par le NER mais le 2e reste en clair (ni span NER intact ni candidat regex ne
le couvre ; être dans paranames ne suffit pas sans candidat).

Fix : post-passe dans process_pdf (étape 3a-bis), après selective_rescan, qui
masque le token majuscule orphelin suivant immédiatement un "[NOM]-". Couvre
le texte ET le raster (NOM_GLOBAL). Réfute la conclusion de Qwen ("paranames
résoudra EJNAINI").

Validation audit_30 (29 docs) : score 98.3 → 98.5/100, LEAK SCORE 100/100
(0 fuite), 0 régression FP. tests/unit 85 passed. BA127127 : EJNAINI 7→0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:02:53 +02:00
9d2fd4052d feat(detect): paranames loader + fallback étendu cross-validation
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>
2026-06-02 15:48:54 +02:00
96f9691395 feat(detect): F2 capture du nom précédant le label "Nom usuel :"
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>
2026-06-02 14:44:59 +02:00
e7380ed258 chore(rgpd): replace remaining CHCB/Bayonne refs after re-verification (D-12)
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>
2026-06-02 14:42:40 +02:00
a1ef2225d5 feat(detect): F3 capture du nom après label "Nom usuel :"
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>
2026-06-02 14:35:33 +02:00
af3fb53772 feat(detect): F1 décomposition noms à trait d'union + F4 filet INSEE opt-in
## 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>
2026-06-02 14:25:52 +02:00
5d89eaf8dc feat(q1): G - B-1 métadonnées sortie (audit.jsonl + XMP PDF)
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>
2026-06-02 10:59:58 +02:00
cf78bea910 feat(q1): F+sécurité — rescan inconditionnel + hardening quarantine
Suite des étapes Q-1 (F = rescan résiduel) + apport sécurité par Qwen
review Codex gpt-5.5 5 rounds (verdict READY FOR MERGE).

## anonymizer_core_refactored_onnx.py

- M5 Rescan résiduel inconditionnel : NIR/EMAIL/IBAN/TEL recherchés après
  TOUT nettoyage. Fail-closed — aucun output livré si > seuil
  (SEUIL_RESCAN_RESIDUEL = 0)
- M3 Return structuré : process_pdf retourne maintenant
  {"status": "quarantined", "reason": ..., "text": "", "audit": ""} au lieu
  de {} sur quarantaine — callers compatibles avec outputs["text"]/"audit"
- C3+M2 fallback préflight : si quarantine_mgr absent ET préflight rate,
  copie du PDF source dans out_dir/_preflight_failed/ avec chmod 0o700
  (le document n'est jamais perdu silencieusement)
- S5 guard double raster : "pdf_raster" not in outputs avant fallback
- Retrait import DocLogger (mort, jamais branché)

## quarantine.py

- _sanitize_doc_name() — anti path-traversal sur le nom de doc
- _escape_markdown_table_cell() — anti injection markdown dans INDEX.md
- _secure_quarantine_dir() — mkdir + chmod(0o700) systématique
- _append_errors_log() durci :
  os.open(O_CREAT|O_APPEND|O_WRONLY|O_NOFOLLOW, 0o600)
  + fcntl.flock(LOCK_EX) + os.fchmod
- Retrait DocLogger (code mort identifié en review)
- Retrait REASON_CODES (jamais utilisé)

## Limites connues

- QuarantineManager pas encore wired dans GUI/server.py — les callers
  actuels marchent en fallback (quarantine_mgr=None)
- finalize() + ProcessPoolExecutor : entries worker-local ne mergent pas
  automatiquement (à documenter)

## Validation

- 73 tests unit existants : OK (non-régression)
- 1 test Q-1 happy path : passe (dégelé dans commit suivant)
- Codex gpt-5.5 5 rounds review : READY FOR MERGE

Co-Authored-By: Qwen Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 10:44:52 +02:00
5216a1518e feat(q1): E - B-3 preflight text too short, quarantine direct
É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>
2026-05-29 21:39:47 +02:00
88f268520b feat(q1): D3a - raster fallback + text copy to quarantine on PDF failure
É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>
2026-05-29 18:42:59 +02:00
32e3bbcadd feat(q1): D2 - try/flag PDF redaction failure in process_pdf
É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>
2026-05-29 18:14:36 +02:00
8e71e83872 feat(q1): D1 - import quarantine module + add quarantine_mgr param
Étape D1 du sprint Q-1 (sous-commit 1/3 pour process_pdf) :

- Import try/except de quarantine.py : QuarantineManager, DocLogger,
  SEUIL_TEXTE_MINI (=100), SEUIL_RESCAN_RESIDUEL (=0)
- Si quarantine.py absent, fallback None pour rétro-compat (anciennes
  installs continuent avec ancien comportement silencieux)
- Nouveau param dans process_pdf : quarantine_mgr (Optional, default None)
- Aucun changement de comportement à ce stade — D2 branchera l'usage

Tests : import OK, process_pdf signature étendue (13 params), SEUIL_TEXTE_MINI
accessible depuis le module.

Ref: docs/coordination/inbox/for-dom/2026-05-29_consolide_pseudocode-Q1-v2.md §10

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 18:12:42 +02:00
7079b029a7 fix(q1): redact_pdf_vector raise on apply_redactions failure
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>
2026-05-29 18:01:29 +02:00
13730d114b feat(admin_rules): CLI simulate_admin_rule + fix email avant force_terms
- 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>
2026-04-28 12:02:17 +02:00
e0b526b2c7 fix(detect): établissements multi-ligne, CHCB en fin de phrase, ville après [ETAB] (#3 #4 #5)
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>
2026-04-28 11:32:45 +02:00
c7e71072e7 fix(detect): RPPS avec qualificateur (RPPS prescripteur :, RPPS de garde :…) (#1)
É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>
2026-04-27 22:33:01 +02:00
7242b5350e fix(detect): labels structurels Nom de jeune fille / Prénom / Ville (#7 #8 #9)
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>
2026-04-27 22:30:40 +02:00
c24b7f6f27 fix(detect): quick wins #6 #10 #11 — caractère ñ, numéro adhérent, NIR avant TEL
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>
2026-04-27 21:13:27 +02:00
c3eb50bfbb fix(detect): masquer artefacts noms de fichiers DPI et variante BACTERIO N° venue
- 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>
2026-04-27 16:17:36 +02:00
df5dabf140 Wire admin rules into ONNX anonymizer 2026-04-21 12:10:17 +02:00
500ebc28c2 Externalize dictionaries and add anonymization review corpus 2026-04-21 10:32:57 +02:00
8e43d8d1ae fix(detect): accepter prénoms 3 chars après Dr/Mme (Ute, Eva, Léo…)
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>
2026-04-15 17:21:54 +02:00
e2e2a7c8e3 fix(redact): masquer tokens collés à ponctuation ("Douar,nécessitant")
Fuite détectée lors du QC batch 22 : le nom "Douar" était dans l'audit
(NOM page 6) mais restait visible dans le PDF redacted_vector. Cause :
dans get_text('words') le word était 'Douar,nécessitant' (virgule collée
sans espace). _search_whole_word faisait un == strict après strip des
ponctuations frontières, mais la virgule était au MILIEU — pas stripée.
→ aucun match → aucun rectangle → fuite.

Fix : passe 2 dans _search_whole_word avec regex word-boundary sur le
texte complet du word (pattern `(?<![A-Za-zÀ-ÿ])token(?![A-Za-zÀ-ÿ])`)
+ bbox proportionnelle au ratio chars matched / chars total du word.
Approximation exacte sur polices monospace, précision ±pixels sur
polices proportionnelles — couverte par le rectangle de redaction.

Validation bout-en-bout sur trackare-BA042686-23090597 : "Douar" masqué
(0 page résiduelle). QC strict retombe de 1 anomalie à 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:10:34 +02:00
aa3db69a9b fix(regex): RE_HOPITAL_VILLE accepte les ALL-CAPS (CENTRE HOSPITALIER)
Le pattern type utilisait [Cc]entre\s+[Hh]ospitalier : seule la 1re lettre
de chaque mot était ambidextre, la suite devait être en minuscules. "CENTRE
HOSPITALIER COTE BASQUE" (tout majuscule) échappait → compensé par regex
YAML force_mask_regex "Centre\s+Hospitalier\s+…".

Fix : utiliser (?i:…) case-insensitive localement sur les sous-motifs "type
d'établissement" et "déterminants" (de, du, la…) tout en gardant le nom
propre strict (1re lettre majuscule obligatoire). Évite les FP tout en
capturant les majuscules complètes.

Cas validés :
- "Centre Hospitalier de Bayonne" → match (inchangé)
- "CENTRE HOSPITALIER COTE BASQUE" → match (nouveau)
- "POLYCLINIQUE CÔTE BASQUE SUD" → match (nouveau)
- "CLINIQUE SAINT-JEAN" → match (nouveau)
- "examen hôpital de Bordeaux" → pas de match (exclusion préservée)

Test YAML stripped : CENTRE HOSPITALIER et COTE BASQUE sont maintenant
masqués par ETAB (regex/AC) au lieu de force_term. Après ce fix + Fix #4,
on peut retirer les regex "Centre\s+Hospitalier…" et "Polyclinique…" du YAML.

Non-régression : 122 hits sur trackare-18007562 avec YAML complet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:40:08 +02:00
83769f6e63 feat(ville): énumérations + CP nu + suffixe CEDEX dans règle contextuelle
Trois trous de détection identifiés par l'audit de règles :

1. Énumération "Bordeaux et Bayonne" / "Bordeaux, Bayonne, Biarritz" : la règle
   contextuelle _RE_GEO_BEFORE n'acceptait que des déclencheurs directs (à, de,
   hôpital de, urgences de…). Dans une énumération, la 2ème ville+ échappait.
   Nouvelle passe 2 : propagation mutuelle entre hits AC adjacents liés par
   " et " ou ", ". Itération à point fixe pour chaînes longues. Garde-fou :
   chaque hit ≥ 5 lettres pour éviter FP sur communes courtes homonymes.

2. Code postal encore en chiffres : _RE_GEO_BEFORE n'acceptait que
   [CODE_POSTAL] déjà masqué. Ajout de `\b\d{5}\s+` comme déclencheur pour
   couvrir l'ordre dans lequel _mask_ville_gazetteers est appelée avant le
   masquage du code postal.

3. Suffixe CEDEX : "BAYONNE CEDEX" capturait BAYONNE seul. Extension automatique
   de la capture pour inclure " CEDEX" et " CEDEX N" adjacents.

Cas validés :
- "travaille à Bordeaux et Bayonne" → [VILLE] et [VILLE]
- "Régions : Bordeaux, Bayonne, Biarritz" → 3× [VILLE] (chaîne sans ancre)
- "64109 BAYONNE CEDEX" → [VILLE] (capture CEDEX inclus)
- "charge", "médecin et patient" → aucun FP

Non-régression : 122 hits sur trackare-18007562.

Après ce fix, on peut retirer BAYONNE, BAYONNE CEDEX du YAML force_mask_terms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:37:55 +02:00
e6f3853426 feat(finess): whitelist de mono-mots distinctifs courts (EMBRUNS, etc.)
Le matcher Aho-Corasick FINESS rejetait tous les mono-mots < 10 chars pour
éviter les faux positifs. Conséquence : EMBRUNS (7 chars), présent dans
etablissements_distinctifs.txt, était ignoré et devait être forcé en YAML
(LES EMBRUNS, REED LES EMBRUNS, EMBRUNS BIDART, regex [Ee]mbruns).

Nouveau fichier data/finess/mono_mots_distinctifs.txt contenant la whitelist
curée des mono-mots courts considérés comme distinctifs. Maintenance manuelle
(un mot par ligne, commentaires autorisés). Le matcher accepte un mono-mot
< 10 chars uniquement s'il est dans cette whitelist.

Initialisation : embruns, embrun (documents CHCB "Les Embruns").

Validation :
- _FINESS_AC matche maintenant "les embruns quelque part" et "embruns seul"
- Pas de régression sur trackare-18007562 (122 hits)

Après ce fix + futurs, on pourra retirer LES EMBRUNS / REED LES EMBRUNS /
EMBRUNS BIDART et regex [Ee]mbruns de force_mask_terms du YAML.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:35:16 +02:00
fd95ae5f2a fix(finess): inclure les entjur + supprimer code mort _FINESS_ETAB_NAMES
Deux corrections exploitant mieux les gazetteers FINESS/INSEE pour réduire la
dépendance au YAML force_mask_terms.

1. scripts/build_finess_gazetteers.py : ne lisait que col 1 (finess_et) du CSV.
   Les col 2 (entjur, entité juridique) étaient ignorés. ~48k numéros
   juridiques manqués, dont 640780417 (CHCB entjur) forcé en YAML à cause
   de cette lacune. Fix : lecture col 1 + col 2 avec déduplication.
   Régénération : 101 941 → 150 436 numéros (+48 495).

2. anonymizer_core_refactored_onnx.py :
   - _FINESS_ETAB_NAMES (122k noms) chargé mais jamais consulté après le
     refactoring NER-first (le matching passe par l'Aho-Corasick sur
     etablissements_distinctifs.txt). Suppression → -122k entrées RAM.
   - _INSEE_PRENOMS (lowercase) et _INSEE_PRENOMS_SET (uppercase sans accents)
     lisaient deux fois le même fichier prenoms_france.txt. Fusion en une
     seule passe disque, les deux formes dérivées en mémoire. -36k lectures.

Validation :
- 640780417 présent dans _FINESS_NUMBERS après rebuild
- 122 hits sur trackare-18007562 (non-régression)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:33:07 +02:00
8e458c16ca fix(frozen): data/*.txt dans bundle, feedback UI pendant chargement modèles
Plantages signalés sous Windows : causes identifiées et corrigées.

1. anonymisation_onefile.spec : les fichiers data/stopwords_manuels.txt,
   villes_blacklist.txt, dpi_labels_blacklist.txt, companion_blacklist.txt
   n'étaient PAS inclus dans le bundle PyInstaller (seuls les sous-dossiers
   data/bdpm, data/finess, data/insee l'étaient). Résultat en frozen : sets
   vides, qualité dégradée, plus de faux positifs.

2. anonymizer_core_refactored_onnx.py : chargements robustifiés.
   - Helper _load_txt_set avec try/except et logging WARNING si fichier absent
   - Fallbacks intégrés (_DPI_LABELS_FALLBACK, _COMPANION_BLACKLIST_FALLBACK)
     pour continuer à fonctionner si bundle partiel
   - try/except sur stopwords_manuels.txt, villes_blacklist.txt, BDPM

3. launcher.py : UX repensée pour le chargement des modèles.
   - SetupWindow (premier lancement) : auto-démarrage (plus de clic nécessaire),
     progress bar avec étapes visuelles (/✓/✗ par modèle), bouton relance si
     échec, bouton "continuer malgré tout" pour modèles optionnels.
   - Splash screen ajouté dans launch_gui() : le chargement des gazetteers
     (INSEE 200k+ noms, FINESS 100k+ établissements) prend 15-30 s au démarrage
     normal. Sans feedback, l'utilisateur croyait l'app plantée. Le splash
     tourne pendant l'import (thread séparé, poll avec splash.after).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:50:42 +02:00
59acf390f4 refactor: externaliser DPI labels et companion blacklist (modifiables sans recompiler)
Suite de l'externalisation des règles. Trois listes étaient codées en dur dans
anonymizer_core_refactored_onnx.py et impossibles à modifier par les
établissements sans recompiler :

- _NEVER_MASK_AS_NAME (12 entrées) — labels DPI structurels
- _DPI_LABELS_BLACKLIST (14 entrées, doublon partiel du précédent)
- _COMPANION_BLACKLIST (~75 entrées) — spécialités, labos pharma, mots ambigus

Les deux premières fusionnées dans data/dpi_labels_blacklist.txt (11 entrées
uniques, comparaison case-insensitive). La troisième dans
data/companion_blacklist.txt (75 entrées, comparaison uppercase).

Ajout de deux clés YAML pour enrichissement par établissement :
- additional_dpi_labels (ex: "Service", "Statut")
- additional_companion_blacklist (ex: spécialités locales)

Les 3 niveaux cumulatifs habituels s'appliquent : code (vide) → fichiers data/
→ YAML config. Chargement au démarrage avec log INFO du nombre d'entrées.

Test trackare-18007562-23054899 : 122 hits, 0 régression, 0 DPI label masqué
comme NOM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:26:18 +02:00
b5058b9c4b fix(whitelist): GUI whitelist_phrases enfin lue et appliquée par le core
Bug majeur depuis l'externalisation : la GUI v5.4 écrivait whitelist_phrases
(clé racine), mais le core ne lisait que whitelist.sections_titres /
noms_maj_excepts (imbriqué). _apply_whitelist post-masquage était par ailleurs
désactivée (c157205) sans remplacement.

Correctif :
- load_dictionaries() lit whitelist_phrases et alimente deux sets globaux
  (_WHITELIST_NEVER_MASK_TOKENS, _WHITELIST_NEVER_MASK_PHRASES). Mots-outils
  (de, du, le...) écartés pour éviter blocages collatéraux.
- _apply_extracted_names : check whitelist en pré-masquage, prime sur les
  force_names (ex: "DUPONT" reste visible même après "Dr DUPONT").
- process_pdf : filtrage final de l'audit avant redact_pdf_vector. Les hits
  multi-mots dont au moins un sous-token est whitelist sont retirés.
- redact_pdf_vector : check whitelist sur les sous-mots cherchés
  individuellement quand le multi-mots n'est pas trouvé sur la page.

Validé sur trackare-18007562-23054899 :
- Avec whitelist BELLEAU : 0 hit dans audit, 31 occurrences préservées dans PDF
- Sans whitelist : 0 occurrence dans PDF (non-régression OK)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:23:09 +02:00
51c75558bc fix: pyzbar FP sur tableaux — carrés noirs sur dates/heures dans les grilles
pyzbar interprétait les cellules de tableaux trackare comme des codes-barres
et les noircissait. Ajout d'un seuil minimum de surface (2000 px²) pour
filtrer les faux positifs sur les petites zones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:27:52 +02:00
2f19f7c470 fix: DR. Ute (3 chars), SAINT-GERMES composé, SODIUM MACO/BAX pharma
- force_names bypass le seuil 4 chars (prénoms courts après Dr/Mme : Ute, Eva)
- SAINT seul = bloqué, SAINT-xxx composé = accepté comme nom
- Labos pharma ajoutés aux stop-words + companion blacklist :
  MACO, AGUETTANT, RENAUDIN, ARROW, BIOGARAN, MYLAN, TEVA, ZENTIVA
- Score : 99.8/100 (amélioration, "Sie" corrigé)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:17:37 +02:00
c157205751 fix: labels DPI masqués (Date, Note, Type, Heure) + whitelist désactivée
- Whitelist post-masquage désactivée : injectait des phrases au mauvais
  endroit dans le texte anonymisé (bug critique)
- Labels DPI "Date", "Note", "Heure", "Type", "Saint", "Page" ajoutés à
  _NEVER_MASK_AS_NAME et _DPI_LABELS_BLACKLIST pour empêcher leur
  propagation globale comme noms de personnes
- Corrige "Date d'admission → [NOM] d'admission",
  "Note d'évolution → [NOM] d'évolution", etc.

Score évaluation : 99.3/100 (fuites pré-existantes Sie/GRAND inchangées)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:07:51 +02:00
4d33610655 fix: cross-validation respecte bypass_stopwords pour les noms forcés (Dr/Mme)
Les noms avec bypass_stopwords=True (contexte Dr/Mme confirmé) sont
maintenant toujours acceptés par la cross-validation, même s'ils sont
dans les stop-words médicaux (ex: Dr MASSE, Dr GRAND).

Note: les fuites "Sie" (3 chars) et "GRAND" (stop-word) existaient
déjà avant le refactoring NER-first (score 99.3 identique).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 11:07:59 +02:00
2a4b9d79a1 Revert "refactor: réduction stop-words manuels — NER cross-validation suffit"
This reverts commit fb7896f88d.
2026-03-31 11:04:51 +02:00
fb7896f88d refactor: réduction stop-words manuels — NER cross-validation suffit
La cross-validation NER (_cross_validate_name_candidates) gère désormais
les décisions contextuelles nom/terme-médical. Les stop-words purement
médicaux sont supprimés :

- data/stopwords_manuels.txt : 1307 → 233 entrées (uniquement les mots
  ambigus qui sont aussi des noms/prénoms INSEE)
- _MEDICAL_STOP_WORDS_SET hardcodé : ~400 → 80 entrées essentielles
  (mots courts, formes galéniques, titres hospitaliers)
- Les enrichissements BDPM (~7300), edsnlp (~2000) et fichier externe
  sont conservés tels quels

Score qualité inchangé : 100/100 (A+), 0 fuite, 0 faux positif.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:26:54 +02:00
22fbf1c772 feat(ner-first): integrate NER-first flow into pipeline (steps 5-6)
Step 5: anonymise_document_regex now accepts optional NER managers,
runs NER on the original (unmasked) text, and cross-validates
regex-extracted names against NER detections + INSEE gazetteers.
NER-only detections (names found by NER but missed by regex) are
also added. Falls back to original behavior when no NER is available.

Step 6: process_pdf passes NER managers into anonymise_document_regex
for NER-first cross-validation. The existing NER safety net pass on
masked text is preserved (double-pass: original + masked text).

Quality score: 100.0/100 (A+), zero regression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 08:38:56 +02:00
23e19e17e4 feat(ner-first): add NER-first architecture scaffolding (steps 1-4)
Add infrastructure for NER-first name validation without changing
existing behavior. New code only, quality score remains 100/100.

Step 1: Load INSEE family names (219K) and prenoms (33K) as
  module-level gazetteers (_INSEE_NOMS_FAMILLE, _INSEE_PRENOMS_SET)
  normalized uppercase without accents.

Step 2: Add _run_ner_on_original_text() that runs all available NER
  models (EDS-Pseudo, GLiNER, CamemBERT-bio) on unmasked text and
  returns deduplicated NerDetection list.

Step 3: Add NerDetection and NameCandidate dataclasses. Modify
  _extract_document_names and _extract_trackare_identity to also
  return NameCandidate lists with context_strength (high/medium/low)
  metadata. Callers updated for new return values.

Step 4: Add _cross_validate_name_candidates() implementing decision
  matrix: high context always accepted, medium/low validated against
  NER confirmations, INSEE membership, and stopword filtering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 08:31:44 +02:00
ac5c35ae2d feat: externalisation des listes — stop-words et villes modifiables sans code
Toutes les listes de règles sont maintenant modifiables sans toucher
au code Python :

Fichiers de données (data/) :
  - stopwords_manuels.txt : 1307 termes médicaux/techniques
  - villes_blacklist.txt : 117 communes à ne pas matcher
  - medicaments_stopwords.txt : 7312 médicaments BDPM (existant)
  - Chargés automatiquement au démarrage

Config YAML (dictionnaires.yml) :
  - additional_stopwords : mots supplémentaires par établissement
  - additional_villes_blacklist : villes supplémentaires
  - whitelist_phrases : phrases à ne jamais anonymiser
  - force_mask_terms : mots à toujours masquer

Chaîne de chargement : code dur → fichiers data/ → YAML config
Les 3 niveaux se cumulent (union).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:45:42 +02:00
106f1fcd2e fix: sync texte↔raster + GUI listes whitelist/blacklist améliorées
Bug critique corrigé : les noms forcés (contexte Dr/Mme) comme "MASSE"
étaient masqués dans le texte mais pas dans le PDF raster car filtrés
par les stop-words médicaux. Nouveau kind "NOM_FORCE" qui bypass le
filtre stop-words dans les fonctions de redaction vector et raster.

GUI : remplacement des zones texte brut par des listes interactives
avec champ de saisie + bouton Ajouter + bouton Supprimer, fond coloré
(vert pour whitelist, rose pour blacklist).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:34:51 +02:00
f9fbae1f27 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>
2026-03-30 15:03:08 +02:00
437877e1c8 feat: support multi-formats — DOCX, images, ODT, RTF, TXT, HTML
Nouveau module format_converter.py : conversion automatique vers PDF
avant anonymisation. Formats supportés :
- PDF (passthrough)
- DOCX (python-docx → texte → PDF)
- ODT (odfpy → texte → PDF)
- RTF (striprtf → texte → PDF)
- TXT (texte brut → PDF via PyMuPDF)
- HTML (BeautifulSoup → texte → PDF)
- JPEG/PNG/TIFF/BMP (image embarquée → OCR docTR en aval)

Nouvelle fonction process_document() : wrapper qui gère la conversion
puis appelle process_pdf(). GUI mise à jour pour chercher tous les
formats supportés (plus seulement *.pdf).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:25:26 +01:00
3992b43925 fix: import sys manquant — crash 'name sys is not defined' en mode frozen
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 01:06:58 +01:00
d1bdfb1aca fix: fenêtres fantômes PyInstaller — désactiver ProcessPoolExecutor en mode frozen
ProcessPoolExecutor relançait l'exe pour chaque sous-processus de
rastérisation sous PyInstaller --onefile, créant une fenêtre GUI par page.
En mode frozen, la rastérisation est maintenant séquentielle.

Aussi: remplacement du mutex Windows par un file lock (msvcrt.locking)
plus fiable pour la protection anti-multi-instance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:51:54 +01:00
65a02952c5 fix: retour relecteur #2 — page scannée noire, labels DPI, stop-words
- Page scannée entièrement noire (OGC 258) : les images couvrant > 70%
  de la page ne sont plus noircies (document scanné ≠ logo/signature)
- Labels DPI "Nom [■] naissance" : tokens < 3 chars ("N", "S") exclus
  du raster pour éviter les FP sur les mots courts des labels
- Stop-words enrichis : betascrub, hibiscrub, fresubin, nutrison,
  résorbable, nombreuses, internationale, capsule, alfa, prothèses
- FINESS blacklist : "internationale", "international", "intercommunal"
- "classification [ETABLISSEMENT] de l'infection" → corrigé

Score évaluation maintenu à 100.0/100 (A+)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 12:11:26 +01:00
ad7f1ffa8a fix: FP médicaments dans raster + texte — RE_EXTRACT_STAFF_ROLE + FINESS + stop-words
Bug #1 (critique) : RE_EXTRACT_STAFF_ROLE matchait à l'intérieur des mots
  (IDE dans METOCLOPRAMIDE, AS dans ATORVASTATINE) → ajout \b word boundaries
  et suppression du ? optionnel sur ASH (AS matchait partout)

Bug #2 : raster multi-mots utilisait page.search_for() (substring matching)
  → ajout vérification frontières de mots pour les tokens multi-mots
  dans redact_pdf_raster et redact_pdf_vector

FP FINESS Aho-Corasick :
  - "resistance" (Centre de la Résistance) matchait "résistance aux fluoroquinolones"
  - "radiotherapie" matchait "tumorectomie, radiothérapie et hormonothérapie"
  → ajout blacklist : resistance, radiotherapie, chimiotherapie, etc.

FP villes : "COU" (commune) matchait dans "prurit (cou, décolleté, dos)"
  → ajout COU, DOS, SEIN, BRAS à _VILLE_BLACKLIST

Stop-words : ajout "totale", "partielle", "prothese", "unicompartimentale"

Score évaluation maintenu à 100.0/100 (A+)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 07:11:57 +01:00
2731bc1ce7 feat: OCR docTR par page — plus de seuil global, traite chaque page pauvre individuellement
L'OCR docTR est maintenant déclenché page par page (< 150 chars) au lieu
d'un seuil global sur tout le document. Permet de traiter les documents
mixtes (pages texte + pages scannées) sans pénaliser le temps de traitement
sur les pages déjà riches en texte.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 20:28:27 +01:00
7c05ff9aaf fix: téléphone +33(0) non détecté + noms médecins homonymes de termes médicaux
- RE_TEL : ajout du format +33(0)XXXXXXXXX (ex: +33(0)156125400)
- _add_tokens_force_first : tous les tokens après Dr/Mme/Mr sont maintenant
  dans force_names (bypass stop-words médicaux). Corrige la fuite de noms
  de médecins homonymes de termes médicaux (ex: Dr MASSE)

Score évaluation maintenu à 100.0/100 (A+)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:33:32 +01:00
27d19ebed7 fix: corrections retours relecteur — fuites adresses/établissements + FP médicaments
Fuites corrigées :
- "Le BOURG" : nouveau regex RE_LIEU_DIT_SEUL pour lieux-dits courants
- "CABINET ETXEBARNONDOA" : nouveau regex RE_EXTRACT_CABINET
- "REED LES EMBRUNS" : ajouté force_mask_terms + force_mask_regex case-insensitive
- "au [ETABLISSEMENT] nocturne" : "long cours" exclu des phrases FINESS

Faux positifs corrigés :
- "OXYGENE LUNETTES" : "lunettes" ajouté aux stop-words
- "POTASSIUM CHLORURE" : "chlorure" ajouté aux stop-words
- Phrases FINESS génériques étendues (le bourg, le val, les pins...)

Score évaluation maintenu à 100.0/100 (A+)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 18:04:08 +01:00
49ff464e6e feat: réduction FP + gazetteers adresses FINESS + batch parallèle + corrections multi-axes
- Token min length relevé de 2-3 → 4 chars (élimine FP EPO, IRC, SIB...)
- Stop-words enrichis : acronymes médicaux 3 lettres, termes pharma, soins infirmiers
- BDPM stop-words : ~7300 noms commerciaux + DCI/substances actives
- Gazetteers adresses FINESS : 63K patterns Aho-Corasick (position-preserving normalization)
- Filtre contextuel anatomique pour FINESS établissements
- Nouvelles regex : RE_CIVILITE_COMMA_LIST, RE_EXTRACT_NOM_UTILISE, RE_EXTRACT_PRENOM,
  RE_NUM_EXAMEN_PATIENT, RE_ADRESSE_LIEU_DIT, RE_CIVILITE_INITIALE, Dr X.NOM
- URLs complètes (RE_URL) + détection multiline
- N° venue inversé (layout-aware) + EPISODE/NDA dans _CRITICAL_PII_TYPES
- HospitalFilter désactivé pour ADRESSE/TEL/VILLE/EPISODE (identifient le patient)
- Batch silver export parallélisé (multiprocessing spawn, N workers)
- Seuil sur-masquage relevé à 8%, server.py enrichi (source regex/ner)
- Blacklist villes : COURANT, PARIS ; contexte villes étendu (UHCD, spécialités)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 09:26:56 +01:00