Commit Β·
28cbb70
1
Parent(s): 4919e03
Multi-pass OCR + Nemotron 8B: drug-focused pass, Llama-3.1-Nemotron-Nano-8B-v1
Browse files- 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-
|
| 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 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
DATE: [prescription date]
|
|
|
|
| 110 |
Rx:
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
-
|
| 119 |
-
-
|
| 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", "
|
| 785 |
-
("Nemotron
|
| 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 |
-
"""
|
| 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 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 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 |
-
|
| 1090 |
-
|
| 1091 |
-
return
|
| 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
|
| 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 |
|