Files
Aivanov_scan_ogc/scratch/test_prompt_recueil_ab2.py
Dom 71f91d9c31 chore(scratch): archives des scripts exploratoires de choix d'OCR
Conservés comme trace de recherche — non documentés, non factorisés,
ne pas dépendre de ce dossier depuis le code de production.

- test_glm_ocr.py          : benchmark GLM-OCR 0.9B (écarté pour
                             faiblesse sur dp_libelle, praticien et
                             colonne Recodage).
- test_got_ocr.py          : tests GOT-OCR2.0 (échec sur tableaux
                             denses à en-têtes verticaux).
- test_paddle.py           : tentative PaddleOCR (incompatible avec
                             paddlepaddle installé).
- test_surya.py            : tentative Surya (incompatible
                             transformers 5.6).
- test_qwen_vl.py          : Qwen2.5-VL-7B (excellent mais 220s/page,
                             écarté faute de VRAM et vitesse).
- test_qwen_vl_3b.py       : Qwen2.5-VL-3B (retenu, 3s/page, qualité
                             > GLM-OCR sur les champs critiques).
- test_prompt_ab.py        : A/B test prompts Accord/Désaccord.
- test_prompt_crop*.py     : prompts + crop ciblé checkboxes (échec
                             → module pipeline/checkboxes.py).
- test_prompt_recueil_*.py : prompts page recueil (consignes verbeuses
                             dégradent la sortie, cf. discussion).
- README.md                : index du dossier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:06:44 +02:00

116 lines
4.9 KiB
Python

"""A/B V1 vs V2 — schéma COMPLET comme base (ancrage maximal)."""
import json
import time
from pathlib import Path
from pipeline.ocr_glm import GLMOCR
from pipeline.ingest import pdf_to_images
from pipeline.extract import parse_json_output
from pipeline.prompts import SCHEMA_RECUEIL as PROMPT_V1_CURRENT
PROMPT_V2 = """Lis cette fiche médicale OGC (contrôle T2A Assurance Maladie) et renvoie STRICTEMENT le JSON ci-dessous, sans commentaire ni markdown.
CONSIGNES IMPORTANTES :
- Le tableau "Codage de l'Établissement / Recodage" a DEUX colonnes distinctes : les codes "Recodage" sont dans la colonne la plus à DROITE, visuellement séparés des codes "Établissement" (à gauche). Ne recopie JAMAIS les codes Établissement dans Recodage. Si la colonne Recodage est vide, laisse vide.
- "dp_libelle" = texte descriptif majuscules qui suit le code DP sur la même ligne (ex: "HEMORR. ET HEMATOME COMPLIQ. UN ACTE, NCA").
- Les 4 valeurs GHM/GHS sont sur UNE SEULE LIGNE en bas, lisibles dans cet ordre : "GHM établissement : XXX GHS établissement : YYY GHM après recodage : ZZZ GHS après recodage : WWW". Extrais les 4 séparément.
- "praticien_conseil" = nom manuscrit (forme "DR + NOM") tout en bas de page, sous "Nom du praticien conseil responsable du codage".
- Les codes CIM-10 commencent TOUJOURS par une LETTRE majuscule (A-Z) suivie de chiffres. JAMAIS par un chiffre. Ex : "I652", "K650", "T814" — jamais "1652".
- Les codes GHM : 2 chiffres + lettre + 3 chiffres (ex: "11M122", "06M033").
- Les codes GHS : nombre à 3-5 chiffres (ex: "4323", "863").
- Si un champ est illisible ou absent, laisse une chaîne vide. Ne devine pas.
{
"etablissement": "",
"finess": "",
"date_debut_controle": "",
"n_ogc": "",
"n_champ": "",
"dates_sejour": "",
"sejour_etab": {
"age": "", "sexe": "", "duree_sejour": "",
"mode_entree": "", "provenance": "",
"mode_sortie": "", "destination": ""
},
"sejour_reco": {
"age": "", "sexe": "", "duree_sejour": "",
"mode_entree": "", "provenance": "",
"mode_sortie": "", "destination": ""
},
"rum_etab": {"um": "", "igs": "", "duree": "", "dates": ""},
"codage_etab": {
"dp": "", "dp_libelle": "", "dr": "",
"das": [{"code": "", "position": "", "libelle": ""}]
},
"codage_reco": {
"dp": "", "dr": "",
"das": [{"code": "", "position": ""}]
},
"actes_etab": [{"code": "", "position": "", "libelle": ""}],
"actes_reco": [{"code": "", "position": ""}],
"ghm_etab": "", "ghs_etab": "",
"ghm_reco": "", "ghs_reco": "",
"recodage_impactant": "",
"ghs_injustifie": "",
"praticien_conseil": ""
}"""
CASES = [("2018 CARC/OGC 7.pdf",1), ("2018 CARC/OGC 27.pdf",1), ("2018 CARC/OGC 55.pdf",1), ("2018 CARC/OGC 86.pdf",1)]
TARGETS = ["codage_etab.dp", "codage_etab.dp_libelle", "codage_reco.dp",
"ghm_etab", "ghs_etab", "ghm_reco", "ghs_reco", "praticien_conseil"]
def get(d, path):
for k in path.split("."):
d = d.get(k, "") if isinstance(d, dict) else ""
return str(d).strip()
def run_prompt(label, prompt, ocr):
print(f"\n### {label}")
scores = {f: 0 for f in TARGETS}
totals = {f: 0 for f in TARGETS}
for pdf, page in CASES:
name = Path(pdf).stem
img = pdf_to_images(pdf)[page-1]
with open(f"output/{name}.json") as f: legacy = json.load(f)["recueil"]["parsed"]
t0 = time.time()
res = ocr.run(img, prompt, max_new_tokens=4096)
parsed = parse_json_output(res["text"]) or {}
print(f" {name} ({time.time()-t0:.1f}s)")
for tf in TARGETS:
v_ext = get(parsed, tf)
v_leg = get(legacy, tf)
# Tolérance dp_libelle : accepter inclusion
if tf == "codage_etab.dp_libelle":
match = v_leg in v_ext if (v_ext and v_leg) else (v_ext == v_leg)
else:
match = v_ext == v_leg
if v_leg: # ne compter que les champs où legacy a une valeur
totals[tf] += 1
if match: scores[tf] += 1
mark = "" if match else ("" if not v_ext and not v_leg else "")
if tf in ("codage_reco.dp", "ghs_reco", "praticien_conseil", "codage_etab.dp_libelle"):
print(f" {mark} {tf:26s} ext={v_ext!r:40s} leg={v_leg!r}")
print(f" --- Score par champ (vs legacy si renseigné) ---")
for tf in TARGETS:
print(f" {tf:26s}: {scores[tf]}/{totals[tf]}")
return scores, totals
def main():
ocr = GLMOCR()
print(f"VRAM = {ocr.vram_gb:.2f} Go")
s1, t1 = run_prompt("V1 (schéma actuel)", PROMPT_V1_CURRENT, ocr)
s2, t2 = run_prompt("V2 (consignes précises)", PROMPT_V2, ocr)
print("\n=========== DELTA V2 - V1 ===========")
for tf in TARGETS:
d = s2[tf] - s1[tf]
mark = "+" if d > 0 else ("-" if d < 0 else "=")
print(f" {mark} {tf:26s} V1={s1[tf]}/{t1[tf]} → V2={s2[tf]}/{t2[tf]}")
if __name__ == "__main__":
main()