From 3f2e2ee9f4a1b3ee273376c45a196c0ce26f4f64 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 24 Apr 2026 15:54:35 +0200 Subject: [PATCH] feat(extract): second passage VLM sur crop colonne Recodage (P0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Qwen ne lit systématiquement que la colonne de gauche du tableau Codage quand on lui donne la page recueil entière : la colonne droite (Recodage) a 27% de couverture en V2.0 avec 100% de validité — une régression majeure puisque c'est le cœur métier du contrôle T2A. Solution : après le passage principal, refaire une extraction dédiée sur un crop zonal de la seule colonne Recodage (y=0.330→0.490 pour exclure le bloc Actes adjacent). Prompt strict anti-hallucination ("beaucoup de lignes sont vides, n'invente rien"). Le résultat écrase partiellement `codage_reco` (DP/DR/DAS) dans le JSON principal. Classification Python par règle métier : - 1er code sans position → DP - 2e code sans position → DR (ignoré si == DP : Qwen duplique parfois) - codes avec position → DAS Filtre CIM-10 par regex en Python pour retirer les codes CCAM (actes) qui pourraient rester si le crop déborde. Ajout d'une env var `QWEN_MAX_PIXELS` (défaut 800) pour ajuster la consommation VRAM sur machines avec GPU partagé (test sur RTX 5070 avec rpa_vision_v3 en parallèle). Ajout de `torch.cuda.empty_cache()` après chaque inférence pour réduire la fragmentation VRAM sur exécutions longues. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline/ocr_qwen.py | 13 +++++++++++-- pipeline/prompts.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/pipeline/ocr_qwen.py b/pipeline/ocr_qwen.py index cb4cdf5..d8cc201 100644 --- a/pipeline/ocr_qwen.py +++ b/pipeline/ocr_qwen.py @@ -28,11 +28,15 @@ class QwenVLOCR: def _init_model(self): t0 = time.time() # max_pixels limite le nombre de patches visuels pour éviter l'OOM - # sur images 300 dpi (2481x3509). ~1.25M pixels = équilibre qualité/VRAM. + # sur images 300 dpi (2481x3509). ~800 patches = équilibre qualité/VRAM, + # tient confortablement dans ~5-6 Go même avec d'autres processus GPU + # en arrière-plan. Configurable via env var QWEN_MAX_PIXELS (en patches). + import os as _os + max_pixels = int(_os.environ.get("QWEN_MAX_PIXELS", 800)) * 28 * 28 self.processor = AutoProcessor.from_pretrained( MODEL_PATH, min_pixels=256 * 28 * 28, - max_pixels=1280 * 28 * 28, + max_pixels=max_pixels, ) self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained( MODEL_PATH, @@ -67,4 +71,9 @@ class QwenVLOCR: generated_ids = self.model.generate(**inputs, max_new_tokens=max_new_tokens) out_ids = generated_ids[:, inputs.input_ids.shape[1]:] output = self.processor.batch_decode(out_ids, skip_special_tokens=True)[0] + # Libérer la VRAM allouée par l'inférence (utile quand d'autres + # processus tournent en parallèle sur le même GPU) + del inputs, generated_ids, out_ids + if torch.cuda.is_available(): + torch.cuda.empty_cache() return {"text": output.strip(), "elapsed_s": time.time() - t0} diff --git a/pipeline/prompts.py b/pipeline/prompts.py index c807bec..a33e97e 100644 --- a/pipeline/prompts.py +++ b/pipeline/prompts.py @@ -54,6 +54,38 @@ Si un champ est illisible, laisse une chaîne vide. Ne devine pas. "praticien_conseil": "" }""" +# --- Second passage dédié : colonne Recodage de la page recueil --- +# Qwen-VL sous-extrait la colonne droite du tableau Codage quand on lui donne +# la page entière (27% de couverture sur `codage_reco.dp` en V2.0). En lui +# donnant directement un crop zonal de cette seule colonne, il lit beaucoup +# mieux (la structure à une seule colonne lève l'ambiguïté). +# +# Zone cropée (coordonnées relatives dans l'image complète) : +# Zone restreinte au seul bloc codage (DP/DR/DAS de la colonne Recodage). +# On exclut la partie Actes (qui commence autour de y=0.680) pour éviter que +# Qwen confonde les codes CCAM (actes) avec des codes CIM-10 (DAS). +RECUEIL_RECODAGE_ZONE = (0.77, 0.330, 0.97, 0.490) + +SCHEMA_RECUEIL_RECODAGE = """Cette image est un extrait d'une colonne d'un tableau médical. +La colonne peut contenir ZÉRO, UN ou PLUSIEURS codes médicaux CIM-10 (format : 1 lettre majuscule + 2 à 4 chiffres, ex: K650, T810, Z954, R31, I652). Un code peut avoir un suffixe `*`. À droite d'un code, une position numérique (1-9) peut être visible. + +IMPORTANT — LIS UNIQUEMENT CE QUI EST PHYSIQUEMENT VISIBLE : +- La plupart des lignes de ce tableau sont VIDES. C'est NORMAL. +- Ne liste QUE les codes effectivement écrits dans l'image. N'INVENTE rien. +- Si l'image ne contient qu'un seul code, ta réponse doit lister exactement un code (pas plusieurs). +- Si l'image ne contient aucun code, renvoie `"codes": []`. +- Ne déduis pas les codes d'autres cases non montrées dans l'image. + +Pour chaque code réellement visible, indique sa position à droite si elle est écrite, sinon "". + +Renvoie STRICTEMENT ce JSON, sans commentaire ni markdown : +{ + "codes": [ + {"code": "", "position": ""} + ] +}""" + + # --- Page 5 : Fiche administrative de concertation 2/2 (décision finale) --- SCHEMA_CONCERTATION_2 = """Lis la fiche de concertation et renvoie STRICTEMENT le JSON suivant, sans commentaire ni markdown. Si un champ est illisible, laisse une chaîne vide.