#!/usr/bin/env python3 """ZIP de démo (Amina + Dom) : capture + JSON de ce que Léa récupère. Règle d'anonymisation (décision Dom 30/06) : on garde TOUT lisible — interface, menus, libellés, valeurs cliniques — et on ne masque QUE l'identité directe du patient, qui se trouve dans le BANDEAU DU HAUT (titre du dossier / onglets). - Capture : floutage CIBLÉ de la bande supérieure uniquement (top_frac). Le reste (menus de navigation, formulaire, valeurs) reste lisible — c'est l'interface qu'on apprend et ce qui sert à naviguer. - JSON : vraies valeurs des champs (lisibles), + une section `patient` où nom / prénom / date de naissance sont remplacés par des tokens. Tourne sur le DGX. Le détail (vraies valeurs) n'est pas affiché par le script — seuls des compteurs et la plage Y floutée le sont (pas de PID dans les logs). """ import argparse import json import sys import zipfile from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from core.llm.ocr_extractor import extract_grid_from_image # noqa: E402 from core.extraction.role_mapper import tokens_from_grid # noqa: E402 from PIL import Image, ImageFilter # noqa: E402 def main(): ap = argparse.ArgumentParser() ap.add_argument("--image", required=True) ap.add_argument("--extraction-json", required=True) ap.add_argument("--out", default="/tmp/demo_lecture_ecran.zip") ap.add_argument("--top-frac", type=float, default=0.15, help="fraction haute de l'écran à flouter (bandeau identité patient)") a = ap.parse_args() grid = extract_grid_from_image(a.image) tokens = tokens_from_grid(grid) fields = json.loads(Path(a.extraction_json).read_text()) img = Image.open(a.image).convert("RGB") H = img.height seuil = int(a.top_frac * H) # Floutage CIBLÉ : uniquement les tokens texte de la bande supérieure # (bandeau d'identité patient). Tout le reste reste lisible. blurred = 0 ys = [] PAD = 2 for t in tokens: if not t.bbox: continue x0, y0, x1, y1 = t.bbox if y0 < seuil: # token dans le bandeau du haut xx0 = max(0, x0 - PAD); yy0 = max(0, y0 - PAD) xx1 = min(img.width, x1 + PAD); yy1 = min(img.height, y1 + PAD) if xx1 > xx0 and yy1 > yy0: region = img.crop((xx0, yy0, xx1, yy1)).filter(ImageFilter.GaussianBlur(12)) img.paste(region, (xx0, yy0)) blurred += 1 ys.append(y0) # JSON démo : vraies valeurs des champs + identité patient tokenisée demo = { "ecran": "Dossier patient — Urgences (DPI réel)", "note": "Données cliniques réelles. Identité directe du patient remplacée par des tokens ; le reste est ce que Léa lit tel quel.", "patient": { "nom": "[nom]", "prenom": "[prenom]", "date_naissance": "[date de naissance]", }, "champs": [ {"label": f.get("label"), "valeur": f.get("value"), "confiance_ocr": round(float(f.get("confidence", 0)), 2), "ancre_ocr": bool(f.get("anchored"))} for f in fields ], } tmp = Path("/tmp/_demo_build"); tmp.mkdir(exist_ok=True) for old in tmp.glob("*"): old.unlink() cap = tmp / "capture.png" img.save(cap) js = tmp / "ce_que_lea_recupere.json" js.write_text(json.dumps(demo, ensure_ascii=False, indent=2)) readme = tmp / "LISEZMOI.txt" readme.write_text( "DÉMO — Lecture d'écran par Léa (RPA 100% vision)\n" "================================================\n\n" "1) capture.png : un vrai écran de dossier patient (Urgences). Tout est\n" " lisible (interface, menus, libellés, valeurs cliniques) ; SEUL le\n" " bandeau d'identité du patient (en haut) est flouté.\n\n" "2) ce_que_lea_recupere.json : ce que Léa extrait de cet écran. L'OCR fournit\n" " les valeurs exactes (vérité), le modèle de vision identifie le RÔLE de\n" " chaque champ. Valeurs cliniques réelles ; identité patient = tokens\n" " [nom]/[prenom]/[date de naissance]. 0 hallucination (valeur = OCR).\n\n" f" {len(demo['champs'])} champs reconnus sur cet écran.\n" ) with zipfile.ZipFile(a.out, "w", zipfile.ZIP_DEFLATED) as z: z.write(cap, cap.name) z.write(js, js.name) z.write(readme, readme.name) plage = f"{min(ys)}..{max(ys)}px" if ys else "—" print(f"# Hauteur image : {H}px | seuil bandeau = {seuil}px (top {a.top_frac:.0%})") print(f"# Tokens floutés (bandeau haut) : {blurred} | plage Y : {plage}") print(f"# Tokens TOTAL : {len(tokens)} (le reste reste lisible)") print(f"# Champs JSON (vraies valeurs) : {len(demo['champs'])}") print(f"# ZIP : {a.out}") if __name__ == "__main__": main()