Commit ·
b8dddf7
1
Parent(s): e898f0a
Require review for low confidence medicine matches
Browse files- gradio_pharmacopilot_demo.py +44 -19
gradio_pharmacopilot_demo.py
CHANGED
|
@@ -53,6 +53,7 @@ MODEL_ID = os.getenv("PHARMACOPILOT_MODEL_ID", "openbmb/MiniCPM-V-4_5")
|
|
| 53 |
LIVE_GPU_OCR = os.getenv("PHARMACOPILOT_LIVE_GPU_OCR", "1").lower() not in {"0", "false", "no"}
|
| 54 |
DEMO_OCR_TEXT = "Neuoxen"
|
| 55 |
DEMO_PROMPT = "Read the handwritten medicine name in the image. Return only the text."
|
|
|
|
| 56 |
OCR_MODEL = None
|
| 57 |
OCR_TOKENIZER = None
|
| 58 |
|
|
@@ -277,7 +278,7 @@ def pipeline_html(stage: int = 0) -> str:
|
|
| 277 |
("Prescription", "captured"),
|
| 278 |
("MiniCPM OCR", "completed"),
|
| 279 |
("Retrieval Engine", "searched"),
|
| 280 |
-
("
|
| 281 |
("Medicine Found", "ready"),
|
| 282 |
]
|
| 283 |
cards = []
|
|
@@ -310,30 +311,51 @@ def medicine_details_html(
|
|
| 310 |
display_name: str,
|
| 311 |
confidence: int,
|
| 312 |
) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
return f"""
|
| 314 |
<div class="result-card">
|
| 315 |
<h3>Prescription Details</h3>
|
| 316 |
<dl class="details">
|
| 317 |
-
<dt>Medicine</dt><dd>{
|
| 318 |
-
<dt>Generic</dt><dd>{
|
| 319 |
-
<dt>Strength</dt><dd>{
|
| 320 |
-
<dt>Manufacturer</dt><dd>{
|
| 321 |
<dt>Confidence</dt><dd>{confidence}%</dd>
|
| 322 |
-
<dt>Category</dt><dd>{
|
| 323 |
-
<dt>Price</dt><dd>
|
| 324 |
</dl>
|
| 325 |
<div class="explain">
|
| 326 |
<h4>AI Explanation</h4>
|
| 327 |
<p><b>OCR detected:</b> "{ocr_text}"</p>
|
| 328 |
-
<p><b>Retrieved:</b> {display_name}</p>
|
| 329 |
-
<p><b>
|
| 330 |
-
<p><b>Inventory:</b>
|
| 331 |
</div>
|
| 332 |
</div>
|
| 333 |
"""
|
| 334 |
|
| 335 |
|
| 336 |
-
def package_status_html(inventory: dict[str, Any]) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
status = "In Stock" if inventory.get("quantity", 0) > 0 else "Out of Stock"
|
| 338 |
dot_class = "ok" if inventory.get("quantity", 0) > 0 else "bad"
|
| 339 |
return f"""
|
|
@@ -360,11 +382,12 @@ def candidates_html(candidates: list[dict[str, Any]]) -> str:
|
|
| 360 |
"""
|
| 361 |
|
| 362 |
|
| 363 |
-
def ocr_compare_html(medicine: dict[str, Any], ocr_text: str, display_name: str) -> str:
|
|
|
|
| 364 |
return f"""
|
| 365 |
<div class="compare-grid">
|
| 366 |
<div><span>OCR Output</span><strong>{ocr_text}</strong></div>
|
| 367 |
-
<div><span>AI Corrected</span><strong>{
|
| 368 |
<div><span>Canonical</span><strong>{medicine['name']}</strong></div>
|
| 369 |
</div>
|
| 370 |
"""
|
|
@@ -433,21 +456,23 @@ def analyze_prescription(image, progress=gr.Progress()):
|
|
| 433 |
|
| 434 |
for pct, label in [
|
| 435 |
(0.70, "Retrieval search over medicine aliases"),
|
| 436 |
-
(0.88, "
|
| 437 |
-
(1.00, "
|
| 438 |
]:
|
| 439 |
progress(pct, desc=label)
|
| 440 |
time.sleep(0.25)
|
| 441 |
|
| 442 |
medicine, candidates, display_name, confidence = find_medicine_from_ocr(ocr_text)
|
|
|
|
| 443 |
inventory = get_inventory(medicine)
|
| 444 |
image_path = resolve_asset_path(medicine.get("image_path"))
|
| 445 |
-
package_image = str(image_path) if image_path else None
|
| 446 |
|
| 447 |
state = {
|
| 448 |
"medicine_id": medicine["id"],
|
| 449 |
"medicine_name": medicine["name"],
|
| 450 |
"display_name": display_name,
|
|
|
|
| 451 |
"shelf": inventory["shelf"],
|
| 452 |
"row": inventory["row"],
|
| 453 |
}
|
|
@@ -458,12 +483,12 @@ def analyze_prescription(image, progress=gr.Progress()):
|
|
| 458 |
pipeline_html(5),
|
| 459 |
medicine_details_html(medicine, inventory, ocr_text, display_name, confidence),
|
| 460 |
package_image,
|
| 461 |
-
package_status_html(inventory),
|
| 462 |
confidence_gauge(confidence),
|
| 463 |
candidates_html(candidates),
|
| 464 |
-
ocr_compare_html(medicine, ocr_text, display_name),
|
| 465 |
-
gr.update(visible=True),
|
| 466 |
gr.update(visible=True),
|
|
|
|
| 467 |
state,
|
| 468 |
)
|
| 469 |
|
|
|
|
| 53 |
LIVE_GPU_OCR = os.getenv("PHARMACOPILOT_LIVE_GPU_OCR", "1").lower() not in {"0", "false", "no"}
|
| 54 |
DEMO_OCR_TEXT = "Neuoxen"
|
| 55 |
DEMO_PROMPT = "Read the handwritten medicine name in the image. Return only the text."
|
| 56 |
+
ACCEPTANCE_THRESHOLD = int(os.getenv("PHARMACOPILOT_ACCEPTANCE_THRESHOLD", "75"))
|
| 57 |
OCR_MODEL = None
|
| 58 |
OCR_TOKENIZER = None
|
| 59 |
|
|
|
|
| 278 |
("Prescription", "captured"),
|
| 279 |
("MiniCPM OCR", "completed"),
|
| 280 |
("Retrieval Engine", "searched"),
|
| 281 |
+
("Validation", "checked"),
|
| 282 |
("Medicine Found", "ready"),
|
| 283 |
]
|
| 284 |
cards = []
|
|
|
|
| 311 |
display_name: str,
|
| 312 |
confidence: int,
|
| 313 |
) -> str:
|
| 314 |
+
accepted = confidence >= ACCEPTANCE_THRESHOLD
|
| 315 |
+
medicine_label = display_name if accepted else "Needs pharmacist review"
|
| 316 |
+
generic_label = medicine.get("name", "Unknown") if accepted else f"Suggestion: {medicine.get('name', 'Unknown')}"
|
| 317 |
+
strength_label = first_strength(medicine.get("strength", "")) if accepted else "Not confirmed"
|
| 318 |
+
manufacturer_label = (medicine.get("manufacturer") or "Not listed") if accepted else "Not confirmed"
|
| 319 |
+
category_label = medicine.get("category", "General") if accepted else "Not confirmed"
|
| 320 |
+
price_label = "PKR 145" if accepted else "Not confirmed"
|
| 321 |
+
validation_label = "Pending review" if not accepted else "Accepted by retrieval threshold"
|
| 322 |
+
inventory_label = (
|
| 323 |
+
f"Shelf {inventory['shelf']}, row {inventory['row']}"
|
| 324 |
+
if accepted
|
| 325 |
+
else "Hidden until a confident medicine match is available"
|
| 326 |
+
)
|
| 327 |
return f"""
|
| 328 |
<div class="result-card">
|
| 329 |
<h3>Prescription Details</h3>
|
| 330 |
<dl class="details">
|
| 331 |
+
<dt>Medicine</dt><dd>{medicine_label}</dd>
|
| 332 |
+
<dt>Generic</dt><dd>{generic_label}</dd>
|
| 333 |
+
<dt>Strength</dt><dd>{strength_label}</dd>
|
| 334 |
+
<dt>Manufacturer</dt><dd>{manufacturer_label}</dd>
|
| 335 |
<dt>Confidence</dt><dd>{confidence}%</dd>
|
| 336 |
+
<dt>Category</dt><dd>{category_label}</dd>
|
| 337 |
+
<dt>Price</dt><dd>{price_label}</dd>
|
| 338 |
</dl>
|
| 339 |
<div class="explain">
|
| 340 |
<h4>AI Explanation</h4>
|
| 341 |
<p><b>OCR detected:</b> "{ocr_text}"</p>
|
| 342 |
+
<p><b>Retrieved:</b> {display_name} ({medicine.get('name', 'Unknown')})</p>
|
| 343 |
+
<p><b>Validation:</b> {validation_label}</p>
|
| 344 |
+
<p><b>Inventory:</b> {inventory_label}</p>
|
| 345 |
</div>
|
| 346 |
</div>
|
| 347 |
"""
|
| 348 |
|
| 349 |
|
| 350 |
+
def package_status_html(inventory: dict[str, Any], accepted: bool = True) -> str:
|
| 351 |
+
if not accepted:
|
| 352 |
+
return """
|
| 353 |
+
<div class="stock-card">
|
| 354 |
+
<div><span>Available Stock</span><strong>-</strong></div>
|
| 355 |
+
<div><span>Status</span><strong class="bad">Needs Review</strong></div>
|
| 356 |
+
<div><span>Shelf</span><strong>Hidden</strong></div>
|
| 357 |
+
</div>
|
| 358 |
+
"""
|
| 359 |
status = "In Stock" if inventory.get("quantity", 0) > 0 else "Out of Stock"
|
| 360 |
dot_class = "ok" if inventory.get("quantity", 0) > 0 else "bad"
|
| 361 |
return f"""
|
|
|
|
| 382 |
"""
|
| 383 |
|
| 384 |
|
| 385 |
+
def ocr_compare_html(medicine: dict[str, Any], ocr_text: str, display_name: str, confidence: int) -> str:
|
| 386 |
+
corrected = display_name if confidence >= ACCEPTANCE_THRESHOLD else f"Needs review: {display_name}"
|
| 387 |
return f"""
|
| 388 |
<div class="compare-grid">
|
| 389 |
<div><span>OCR Output</span><strong>{ocr_text}</strong></div>
|
| 390 |
+
<div><span>AI Corrected</span><strong>{corrected}</strong></div>
|
| 391 |
<div><span>Canonical</span><strong>{medicine['name']}</strong></div>
|
| 392 |
</div>
|
| 393 |
"""
|
|
|
|
| 456 |
|
| 457 |
for pct, label in [
|
| 458 |
(0.70, "Retrieval search over medicine aliases"),
|
| 459 |
+
(0.88, "Validation check"),
|
| 460 |
+
(1.00, "Result prepared"),
|
| 461 |
]:
|
| 462 |
progress(pct, desc=label)
|
| 463 |
time.sleep(0.25)
|
| 464 |
|
| 465 |
medicine, candidates, display_name, confidence = find_medicine_from_ocr(ocr_text)
|
| 466 |
+
accepted = confidence >= ACCEPTANCE_THRESHOLD
|
| 467 |
inventory = get_inventory(medicine)
|
| 468 |
image_path = resolve_asset_path(medicine.get("image_path"))
|
| 469 |
+
package_image = str(image_path) if image_path and accepted else None
|
| 470 |
|
| 471 |
state = {
|
| 472 |
"medicine_id": medicine["id"],
|
| 473 |
"medicine_name": medicine["name"],
|
| 474 |
"display_name": display_name,
|
| 475 |
+
"accepted": accepted,
|
| 476 |
"shelf": inventory["shelf"],
|
| 477 |
"row": inventory["row"],
|
| 478 |
}
|
|
|
|
| 483 |
pipeline_html(5),
|
| 484 |
medicine_details_html(medicine, inventory, ocr_text, display_name, confidence),
|
| 485 |
package_image,
|
| 486 |
+
package_status_html(inventory, accepted),
|
| 487 |
confidence_gauge(confidence),
|
| 488 |
candidates_html(candidates),
|
| 489 |
+
ocr_compare_html(medicine, ocr_text, display_name, confidence),
|
|
|
|
| 490 |
gr.update(visible=True),
|
| 491 |
+
gr.update(visible=True, interactive=accepted),
|
| 492 |
state,
|
| 493 |
)
|
| 494 |
|