Mohibullah commited on
Commit
9e8339d
·
1 Parent(s): 28cbb70

Refactor medicine search with comprehensive NAME_TO_MED lookup, fix legibility, add openai requirement, add new brand mappings

Browse files
data/training/bd_brand_to_generic.json CHANGED
@@ -479,5 +479,11 @@
479
  "fluclox": "flucloxacillin",
480
  "ambroy": "ambroxol",
481
  "dexter": "dexamethasone",
482
- "dextor": "dexamethasone"
 
 
 
 
 
 
483
  }
 
479
  "fluclox": "flucloxacillin",
480
  "ambroy": "ambroxol",
481
  "dexter": "dexamethasone",
482
+ "dextor": "dexamethasone",
483
+ "ultrafen-plus": "diclofenac",
484
+ "ultrafen plus": "diclofenac",
485
+ "ultrafen": "diclofenac",
486
+ "ultracalc-d": "calcium carbonate",
487
+ "ultracalc d": "calcium carbonate",
488
+ "cartilix": "glucosamine"
489
  }
gradio_pharmacopilot_demo.py CHANGED
@@ -223,6 +223,22 @@ def normalize(text: str) -> str:
223
  return " ".join(text.strip().lower().split())
224
 
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  def clean_prediction(raw_prediction: str) -> str:
227
  """Clean a raw OCR prediction for single-name extraction (legacy helper)."""
228
  text = str(raw_prediction or "").strip()
@@ -258,33 +274,23 @@ def label_for_medicine(ocr_text: str, medicine: dict[str, Any]) -> str:
258
  def find_medicine_from_ocr(ocr_text: str, strength_hint: str | None = None) -> tuple[dict[str, Any], list[dict[str, Any]], str, int]:
259
  """Find medicine from OCR text with optional strength disambiguation."""
260
  query = normalize(ocr_text)
261
- corrected_query = query
262
- canonical = BD_BRAND_TO_GENERIC.get(corrected_query, corrected_query)
263
- direct_medicine = MED_BY_NAME.get(normalize(canonical))
264
 
265
- candidate_names = set()
266
- for med in MEDICINES:
267
- candidate_names.add(med["name"])
268
- candidate_names.add(med.get("generic_name") or med["name"])
269
- candidate_names.update(med.get("brand_names") or [])
270
- candidate_names.update(BD_BRAND_TO_GENERIC.keys())
271
 
272
  scored = []
273
- for name in candidate_names:
274
- score = SequenceMatcher(None, query, normalize(name)).ratio()
275
  if score > 0.35:
276
- mapped = BD_BRAND_TO_GENERIC.get(normalize(name), normalize(name))
277
- med = MED_BY_NAME.get(mapped) or MED_BY_NAME.get(normalize(name))
278
- if med:
279
- # Boost score if strength matches
280
- if strength_hint and med.get("strength"):
281
- if normalize(strength_hint) in normalize(med["strength"]):
282
- score = min(1.0, score + 0.1)
283
- scored.append({"label": name, "medicine": med, "score": score})
284
 
285
  scored.sort(key=lambda item: item["score"], reverse=True)
286
- if direct_medicine:
287
- medicine = direct_medicine
288
  display_name = label_for_medicine(ocr_text, medicine)
289
  primary_score = 0.97
290
  elif scored:
@@ -311,10 +317,10 @@ def find_medicine_from_ocr(ocr_text: str, strength_hint: str | None = None) -> t
311
  fallback_name = get_close_matches(query, list(BD_BRAND_TO_GENERIC.keys()), n=1)
312
  if fallback_name:
313
  mapped = BD_BRAND_TO_GENERIC[fallback_name[0]]
314
- med = MED_BY_NAME.get(mapped)
315
- if med and med["id"] not in seen_ids:
316
- top.append({"label": fallback_name[0], "medicine": med, "score": 0.62})
317
- seen_ids.add(med["id"])
318
  continue
319
  break
320
 
@@ -431,6 +437,22 @@ def empty_extraction() -> dict[str, Any]:
431
  }
432
 
433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  def parse_structured_extraction(raw_text: str, ocr_text: str = "") -> dict[str, Any]:
435
  """Parse Nemotron output into the structured extraction schema.
436
  Falls back gracefully if JSON is malformed."""
@@ -459,6 +481,12 @@ def parse_structured_extraction(raw_text: str, ocr_text: str = "") -> dict[str,
459
  if drug_val and is_controlled_substance(drug_val):
460
  extraction["document_metadata"]["is_controlled_substance"] = True
461
 
 
 
 
 
 
 
462
  return extraction
463
 
464
 
 
223
  return " ".join(text.strip().lower().split())
224
 
225
 
226
+ # Build a comprehensive lookup map: normalized name -> (original casing, medicine dict)
227
+ NAME_TO_MED = {}
228
+ for m in MEDICINES:
229
+ NAME_TO_MED[normalize(m["name"])] = (m["name"], m)
230
+ if m.get("generic_name"):
231
+ NAME_TO_MED[normalize(m["generic_name"])] = (m["generic_name"], m)
232
+ for brand in m.get("brand_names") or []:
233
+ NAME_TO_MED[normalize(brand)] = (brand, m)
234
+
235
+ for brand, generic in BD_BRAND_TO_GENERIC.items():
236
+ norm_gen = normalize(generic)
237
+ res = NAME_TO_MED.get(norm_gen)
238
+ if res:
239
+ NAME_TO_MED[normalize(brand)] = (brand, res[1])
240
+
241
+
242
  def clean_prediction(raw_prediction: str) -> str:
243
  """Clean a raw OCR prediction for single-name extraction (legacy helper)."""
244
  text = str(raw_prediction or "").strip()
 
274
  def find_medicine_from_ocr(ocr_text: str, strength_hint: str | None = None) -> tuple[dict[str, Any], list[dict[str, Any]], str, int]:
275
  """Find medicine from OCR text with optional strength disambiguation."""
276
  query = normalize(ocr_text)
 
 
 
277
 
278
+ # Direct lookup first
279
+ direct_res = NAME_TO_MED.get(query)
 
 
 
 
280
 
281
  scored = []
282
+ for norm_name, (orig_name, med) in NAME_TO_MED.items():
283
+ score = SequenceMatcher(None, query, norm_name).ratio()
284
  if score > 0.35:
285
+ # Boost score if strength matches
286
+ if strength_hint and med.get("strength"):
287
+ if normalize(strength_hint) in normalize(med["strength"]):
288
+ score = min(1.0, score + 0.1)
289
+ scored.append({"label": orig_name, "medicine": med, "score": score})
 
 
 
290
 
291
  scored.sort(key=lambda item: item["score"], reverse=True)
292
+ if direct_res:
293
+ medicine = direct_res[1]
294
  display_name = label_for_medicine(ocr_text, medicine)
295
  primary_score = 0.97
296
  elif scored:
 
317
  fallback_name = get_close_matches(query, list(BD_BRAND_TO_GENERIC.keys()), n=1)
318
  if fallback_name:
319
  mapped = BD_BRAND_TO_GENERIC[fallback_name[0]]
320
+ res = NAME_TO_MED.get(normalize(mapped))
321
+ if res and res[1]["id"] not in seen_ids:
322
+ top.append({"label": fallback_name[0], "medicine": res[1], "score": 0.62})
323
+ seen_ids.add(res[1]["id"])
324
  continue
325
  break
326
 
 
437
  }
438
 
439
 
440
+ def calculate_fallback_legibility(extraction: dict[str, Any]) -> float:
441
+ scores = []
442
+ for section_key in ("patient_info", "prescriber_info", "prescription_details"):
443
+ section = extraction.get(section_key, {})
444
+ for field_key, field in section.items():
445
+ if isinstance(field, dict):
446
+ val = field.get("value")
447
+ conf = field.get("confidence", 0.0)
448
+ # Only average fields that were actually detected and not false/empty
449
+ if val is not None and val != "" and val is not False:
450
+ scores.append(conf)
451
+ if not scores:
452
+ return 0.0
453
+ return sum(scores) / len(scores)
454
+
455
+
456
  def parse_structured_extraction(raw_text: str, ocr_text: str = "") -> dict[str, Any]:
457
  """Parse Nemotron output into the structured extraction schema.
458
  Falls back gracefully if JSON is malformed."""
 
481
  if drug_val and is_controlled_substance(drug_val):
482
  extraction["document_metadata"]["is_controlled_substance"] = True
483
 
484
+ # Fallback legibility calculation if overall_legibility_score is 0.0
485
+ metadata = extraction.setdefault("document_metadata", {})
486
+ legibility = metadata.get("overall_legibility_score", 0.0)
487
+ if legibility == 0.0:
488
+ metadata["overall_legibility_score"] = calculate_fallback_legibility(extraction)
489
+
490
  return extraction
491
 
492
 
requirements.txt CHANGED
@@ -10,3 +10,4 @@ sentencepiece
10
  protobuf
11
  einops
12
  timm
 
 
10
  protobuf
11
  einops
12
  timm
13
+ openai