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>
116 lines
4.9 KiB
Python
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()
|