Mohibullah commited on
Commit
28cbb70
Β·
1 Parent(s): 4919e03

Multi-pass OCR + Nemotron 8B: drug-focused pass, Llama-3.1-Nemotron-Nano-8B-v1

Browse files
Files changed (1) hide show
  1. gradio_pharmacopilot_demo.py +77 -53
gradio_pharmacopilot_demo.py CHANGED
@@ -51,7 +51,7 @@ BRAND_MAP_PATH = data_path("training/bd_brand_to_generic.json")
51
  INVENTORY_PATH = data_path("inventory.json")
52
 
53
  MODEL_ID = os.getenv("PHARMACOPILOT_MODEL_ID", "openbmb/MiniCPM-V-4_5")
54
- NEMOTRON_MODEL_ID = os.getenv("NEMOTRON_MODEL_ID", "nvidia/Nemotron-Mini-4B-Instruct")
55
  NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "")
56
  NVIDIA_BASE_URL = os.getenv("NVIDIA_BASE_URL", "https://integrate.api.nvidia.com/v1")
57
  NVIDIA_NIM_MODEL = os.getenv("NVIDIA_NIM_MODEL", "nvidia/nvidia-nemotron-nano-9b-v2")
@@ -92,35 +92,40 @@ def is_controlled_substance(drug_name: str) -> bool:
92
 
93
  # ── Prompts ──────────────────────────────────────────────────────────────────
94
  # Pass 1: MiniCPM-V reads ALL text from the prescription image
95
- FULL_OCR_PROMPT = """You are a medical prescription OCR engine. This prescription may contain text in multiple scripts (e.g., Bengali, Hindi, Arabic, English). Your job is to read and transcribe ALL text.
96
-
97
- PRIORITY ORDER β€” read these sections first:
98
- 1. HANDWRITTEN CONTENT: Drug names (Tab., Cap., Inj., Syp.), dosages (mg, ml), frequencies (1+0+1, BD, TDS), durations
99
- 2. Patient name and age/date of birth (often near top, after "Name:" or similar)
100
- 3. Date of prescription
101
- 4. Doctor/Prescriber name and credentials (often printed at top or stamped at bottom)
102
- 5. Clinic/Hospital name, address, phone numbers
103
- 6. Any other printed or stamped text
104
-
105
- OUTPUT FORMAT β€” structure your output like this:
106
- DOCTOR: [doctor name and credentials]
107
- CLINIC: [clinic/hospital name and address]
108
- PATIENT: [patient name]
 
 
 
 
 
 
 
 
109
  DATE: [prescription date]
 
110
  Rx:
111
- 1) [drug name] [strength] β€” [frequency/dosage instructions]
112
- 2) [drug name] [strength] β€” [frequency/dosage instructions]
113
- ...
114
- ADVICE: [any additional instructions, follow-up notes]
115
- SIGNATURE: [PRESENT/NOT VISIBLE]
116
-
117
- CRITICAL RULES:
118
- - Drug names are almost always written in English/Latin script (e.g., Tab. Diclofenac, Cap. Omeprazole) even on non-English prescriptions. READ THEM CAREFULLY.
119
- - Dosage patterns like "1+0+1", "2+0+2", "0+0+1" mean morning+afternoon+night doses
120
- - Transcribe EXACTLY as written β€” do NOT translate, correct spelling, or expand abbreviations
121
- - Read ALL numbered items (β‘ , β‘‘, β‘’ or 1), 2), 3) etc.)
122
- - If text is illegible, write [ILLEGIBLE]
123
- - Include ALL drugs β€” prescriptions often have 3-10 medications listed"""
124
 
125
  # Pass 2: Nemotron structures the raw OCR into the clinical JSON schema
126
  STRUCTURING_PROMPT_TEMPLATE = """You are a HIPAA-compliant Clinical Data Extraction Agent.
@@ -781,8 +786,8 @@ def pipeline_html(stage: int = 0, validation_status: str = "waiting") -> str:
781
  }.get(validation_status, "Nemotron Review")
782
  steps = [
783
  ("Prescription", "uploaded"),
784
- ("MiniCPM OCR", "full text extraction"),
785
- ("Nemotron Parse", "structured JSON"),
786
  ("Retrieval Engine", "ranked candidates"),
787
  (validation_label, "returned a decision"),
788
  ]
@@ -1046,8 +1051,36 @@ def ocr_compare_html(
1046
 
1047
  # ── OCR Function (Pass 1: MiniCPM-V full text extraction) ────────────────────
1048
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1049
  def run_minicpm_ocr(pil_image: Image.Image) -> str:
1050
- """Pass 1: Use MiniCPM-V to read ALL text from the prescription image."""
1051
  global OCR_MODEL, OCR_TOKENIZER
1052
 
1053
  try:
@@ -1067,28 +1100,19 @@ def run_minicpm_ocr(pil_image: Image.Image) -> str:
1067
  if torch.cuda.is_available():
1068
  OCR_MODEL = OCR_MODEL.cuda()
1069
 
1070
- messages = [{"role": "user", "content": [pil_image.convert("RGB"), FULL_OCR_PROMPT]}]
1071
- kwargs = {
1072
- "image": None,
1073
- "msgs": messages,
1074
- "tokenizer": OCR_TOKENIZER,
1075
- "sampling": False,
1076
- "stream": False,
1077
- "max_new_tokens": 1024,
1078
- "enable_thinking": False,
1079
- "temperature": 0.0,
1080
- "top_p": 0.1,
1081
- }
1082
- try:
1083
- raw_prediction = OCR_MODEL.chat(**kwargs)
1084
- except TypeError:
1085
- kwargs.pop("temperature", None)
1086
- kwargs.pop("top_p", None)
1087
- raw_prediction = OCR_MODEL.chat(**kwargs)
1088
 
1089
- if not isinstance(raw_prediction, str):
1090
- raw_prediction = "".join(list(raw_prediction))
1091
- return raw_prediction.strip()
1092
 
1093
 
1094
  # ── Main Analysis Pipeline ───────────────────────────────────────────────────
@@ -1104,7 +1128,7 @@ def analyze_prescription(image, progress=gr.Progress()):
1104
  time.sleep(0.1)
1105
 
1106
  # Step 2: MiniCPM-V full text OCR
1107
- progress(0.20, desc="MiniCPM-V reading full prescription text...")
1108
  ocr_text = run_minicpm_ocr(image)
1109
  unload_ocr_model()
1110
 
 
51
  INVENTORY_PATH = data_path("inventory.json")
52
 
53
  MODEL_ID = os.getenv("PHARMACOPILOT_MODEL_ID", "openbmb/MiniCPM-V-4_5")
54
+ NEMOTRON_MODEL_ID = os.getenv("NEMOTRON_MODEL_ID", "nvidia/Llama-3.1-Nemotron-Nano-8B-v1")
55
  NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "")
56
  NVIDIA_BASE_URL = os.getenv("NVIDIA_BASE_URL", "https://integrate.api.nvidia.com/v1")
57
  NVIDIA_NIM_MODEL = os.getenv("NVIDIA_NIM_MODEL", "nvidia/nvidia-nemotron-nano-9b-v2")
 
92
 
93
  # ── Prompts ──────────────────────────────────────────────────────────────────
94
  # Pass 1: MiniCPM-V reads ALL text from the prescription image
95
+ # Pass 1A: Focused drug extraction β€” short, direct prompt to force reading Latin-script drug names
96
+ DRUG_FOCUSED_PROMPT = """Look at this prescription image carefully. List ONLY the medicine/drug names and their dosages.
97
+
98
+ Drug names on prescriptions are written in English/Latin letters like:
99
+ - Tab. (tablet), Cap. (capsule), Syp. (syrup), Inj. (injection)
100
+ - Examples: Tab. Paracetamol 500mg, Cap. Amoxicillin 250mg, Tab. Diclofenac 50mg
101
+
102
+ For each drug, write:
103
+ - The drug name exactly as written
104
+ - The strength if visible (e.g., 50mg, 200mg)
105
+ - The dosage pattern if visible (e.g., 1+0+1, 2+0+2)
106
+
107
+ List them numbered. If you cannot read a drug name, write [ILLEGIBLE].
108
+ Do NOT translate or explain. Just list the drugs."""
109
+
110
+ # Pass 1B: Full prescription text extraction
111
+ FULL_OCR_PROMPT = """Read this medical prescription image. It may have Bengali/Hindi/Urdu printed headers and English handwritten content.
112
+
113
+ Extract ALL information in this format:
114
+ DOCTOR: [name and credentials from printed header or stamp]
115
+ CLINIC: [clinic/hospital name]
116
+ PATIENT: [patient name β€” usually handwritten near top]
117
  DATE: [prescription date]
118
+ CHIEF COMPLAINT: [the medical condition/reason for visit if noted]
119
  Rx:
120
+ [list all drugs with strengths and dosage patterns]
121
+ ADVICE: [follow-up instructions]
122
+ SIGNATURE: [PRESENT or NOT VISIBLE]
123
+
124
+ RULES:
125
+ - Drug names are ALWAYS in English/Latin script (Tab., Cap., Syp.) β€” read them carefully
126
+ - Dosage patterns like "2+0+2" mean morning+afternoon+night
127
+ - Do NOT translate, correct spelling, or interpret β€” transcribe exactly as written
128
+ - Read ALL numbered items"""
 
 
 
 
129
 
130
  # Pass 2: Nemotron structures the raw OCR into the clinical JSON schema
131
  STRUCTURING_PROMPT_TEMPLATE = """You are a HIPAA-compliant Clinical Data Extraction Agent.
 
786
  }.get(validation_status, "Nemotron Review")
787
  steps = [
788
  ("Prescription", "uploaded"),
789
+ ("MiniCPM OCR", "2-pass extraction"),
790
+ ("Nemotron 8B", "structured JSON"),
791
  ("Retrieval Engine", "ranked candidates"),
792
  (validation_label, "returned a decision"),
793
  ]
 
1051
 
1052
  # ── OCR Function (Pass 1: MiniCPM-V full text extraction) ────────────────────
1053
 
1054
+ def _run_minicpm_single_pass(pil_image: Image.Image, prompt: str, max_tokens: int = 512) -> str:
1055
+ """Run a single MiniCPM-V inference pass with the given prompt."""
1056
+ global OCR_MODEL, OCR_TOKENIZER
1057
+
1058
+ messages = [{"role": "user", "content": [pil_image.convert("RGB"), prompt]}]
1059
+ kwargs = {
1060
+ "image": None,
1061
+ "msgs": messages,
1062
+ "tokenizer": OCR_TOKENIZER,
1063
+ "sampling": False,
1064
+ "stream": False,
1065
+ "max_new_tokens": max_tokens,
1066
+ "enable_thinking": False,
1067
+ "temperature": 0.0,
1068
+ "top_p": 0.1,
1069
+ }
1070
+ try:
1071
+ raw = OCR_MODEL.chat(**kwargs)
1072
+ except TypeError:
1073
+ kwargs.pop("temperature", None)
1074
+ kwargs.pop("top_p", None)
1075
+ raw = OCR_MODEL.chat(**kwargs)
1076
+
1077
+ if not isinstance(raw, str):
1078
+ raw = "".join(list(raw))
1079
+ return raw.strip()
1080
+
1081
+
1082
  def run_minicpm_ocr(pil_image: Image.Image) -> str:
1083
+ """Multi-pass OCR: Run focused drug extraction first, then full text extraction, and combine."""
1084
  global OCR_MODEL, OCR_TOKENIZER
1085
 
1086
  try:
 
1100
  if torch.cuda.is_available():
1101
  OCR_MODEL = OCR_MODEL.cuda()
1102
 
1103
+ # Pass 1A: Focused drug extraction (short, direct)
1104
+ drug_pass = _run_minicpm_single_pass(pil_image, DRUG_FOCUSED_PROMPT, max_tokens=512)
1105
+
1106
+ # Pass 1B: Full prescription text extraction
1107
+ full_pass = _run_minicpm_single_pass(pil_image, FULL_OCR_PROMPT, max_tokens=1024)
1108
+
1109
+ # Combine both passes β€” drug-focused pass takes priority for medication data
1110
+ combined = f"""=== DRUG EXTRACTION (focused pass) ===
1111
+ {drug_pass}
 
 
 
 
 
 
 
 
 
1112
 
1113
+ === FULL PRESCRIPTION TEXT ===
1114
+ {full_pass}"""
1115
+ return combined
1116
 
1117
 
1118
  # ── Main Analysis Pipeline ───────────────────────────────────────────────────
 
1128
  time.sleep(0.1)
1129
 
1130
  # Step 2: MiniCPM-V full text OCR
1131
+ progress(0.20, desc="MiniCPM-V multi-pass OCR (drug-focused + full text)...")
1132
  ocr_text = run_minicpm_ocr(image)
1133
  unload_ocr_model()
1134