3v324v23 commited on
Commit
45dc8b6
·
1 Parent(s): 3ea349d

Fix: robust OCR parsing and UI label alignment

Browse files
Files changed (4) hide show
  1. api/alert_logic.py +19 -22
  2. api/extract_report.py +30 -10
  3. api/main.py +3 -3
  4. dashboard/app.js +8 -6
api/alert_logic.py CHANGED
@@ -20,7 +20,7 @@ _alert_history: Dict[str, dict] = {}
20
 
21
  # ─── Clinical rule safety net ─────────────────────────────────────────────────
22
 
23
- def apply_clinical_safety_net(visits_raw: list) -> tuple[str | None, str | None]:
24
  """
25
  Checks raw visit values against WHO clinical thresholds.
26
  Returns (forced_tier, reason) if a rule fires, else (None, None).
@@ -33,16 +33,16 @@ def apply_clinical_safety_net(visits_raw: list) -> tuple[str | None, str | None]
33
  latest_sys = latest.get("systolic_bp") or 0
34
  latest_dia = latest.get("diastolic_bp") or 0
35
 
36
- # ── RED RULES ──────────────────────────────────────────────────────────────
37
 
38
- # Rule 1: Severe hypertension RED
39
  if max_systolic >= 160 or max_diastolic >= 110:
40
- return "RED", (
41
  f"SystolicBP {max_systolic} mmHg meets WHO severe "
42
- f"hypertension threshold (160) emergency referral required"
43
  )
44
 
45
- # Rule 5: Multi-vital simultaneous escalation RED
46
  if len(visits_raw) >= 3:
47
  first = visits_raw[0]
48
  last = visits_raw[-1]
@@ -61,37 +61,37 @@ def apply_clinical_safety_net(visits_raw: list) -> tuple[str | None, str | None]
61
  if temp_rise >= 0.5: escalating_count += 1
62
 
63
  if escalating_count >= 3:
64
- return "RED", (
65
  f"{escalating_count} vitals escalating simultaneously "
66
  f"(BP +{sys_rise:.0f} mmHg, HR +{hr_rise:.0f} bpm, "
67
- f"BS +{bs_rise:.1f} mmol/L) combined deterioration "
68
  f"pattern, refer immediately"
69
  )
70
 
71
- # ── AMBER RULES ────────────────────────────────────────────────────────────
72
 
73
- # Rule 2: Hypertension in pregnancy AMBER
74
  if latest_sys >= 140 or latest_dia >= 90:
75
- return "AMBER", (
76
  f"SystolicBP {latest_sys} mmHg meets WHO hypertension "
77
- f"in pregnancy threshold (140)"
78
  )
79
 
80
- # Rule 3: Severe hyperglycaemia AMBER
81
  if max_bs > 11.1:
82
- return "AMBER", (
83
  f"Blood sugar {max_bs} mmol/L exceeds gestational "
84
  f"diabetes threshold (>11.1)"
85
  )
86
 
87
- # Rule 4: BP escalation pattern AMBER
88
  if len(visits_raw) >= 2:
89
  first_sys = visits_raw[0].get("systolic_bp") or 0
90
  bp_rise = latest_sys - first_sys
91
  if bp_rise >= 20:
92
- return "AMBER", (
93
  f"SystolicBP rose {bp_rise:.0f} mmHg across visits "
94
- f" escalation pattern detected"
95
  )
96
 
97
  return None, None
@@ -120,11 +120,8 @@ def compute_alert_tier(
120
 
121
  # Determine tier
122
  if high_risk_prob >= RED_THRESHOLD:
123
- recent_weight = sum(visit_importances[-RED_CONSECUTIVE_VISITS:])
124
- if recent_weight > 0.4:
125
- tier = AlertTier.RED
126
- else:
127
- tier = AlertTier.AMBER
128
  elif high_risk_prob >= AMBER_THRESHOLD:
129
  tier = AlertTier.AMBER
130
  else:
 
20
 
21
  # ─── Clinical rule safety net ─────────────────────────────────────────────────
22
 
23
+ def apply_clinical_safety_net(visits_raw: list) -> tuple[AlertTier | None, str | None]:
24
  """
25
  Checks raw visit values against WHO clinical thresholds.
26
  Returns (forced_tier, reason) if a rule fires, else (None, None).
 
33
  latest_sys = latest.get("systolic_bp") or 0
34
  latest_dia = latest.get("diastolic_bp") or 0
35
 
36
+ # --- RED RULES --------------------------------------------------------------
37
 
38
+ # Rule 1: Severe hypertension -> RED
39
  if max_systolic >= 160 or max_diastolic >= 110:
40
+ return AlertTier.RED, (
41
  f"SystolicBP {max_systolic} mmHg meets WHO severe "
42
+ f"hypertension threshold (>=160) -- emergency referral required"
43
  )
44
 
45
+ # Rule 5: Multi-vital simultaneous escalation -> RED
46
  if len(visits_raw) >= 3:
47
  first = visits_raw[0]
48
  last = visits_raw[-1]
 
61
  if temp_rise >= 0.5: escalating_count += 1
62
 
63
  if escalating_count >= 3:
64
+ return AlertTier.RED, (
65
  f"{escalating_count} vitals escalating simultaneously "
66
  f"(BP +{sys_rise:.0f} mmHg, HR +{hr_rise:.0f} bpm, "
67
+ f"BS +{bs_rise:.1f} mmol/L) -- combined deterioration "
68
  f"pattern, refer immediately"
69
  )
70
 
71
+ # --- AMBER RULES ------------------------------------------------------------
72
 
73
+ # Rule 2: Hypertension in pregnancy -> AMBER
74
  if latest_sys >= 140 or latest_dia >= 90:
75
+ return AlertTier.AMBER, (
76
  f"SystolicBP {latest_sys} mmHg meets WHO hypertension "
77
+ f"in pregnancy threshold (>=140)"
78
  )
79
 
80
+ # Rule 3: Severe hyperglycaemia -> AMBER
81
  if max_bs > 11.1:
82
+ return AlertTier.AMBER, (
83
  f"Blood sugar {max_bs} mmol/L exceeds gestational "
84
  f"diabetes threshold (>11.1)"
85
  )
86
 
87
+ # Rule 4: BP escalation pattern -> AMBER
88
  if len(visits_raw) >= 2:
89
  first_sys = visits_raw[0].get("systolic_bp") or 0
90
  bp_rise = latest_sys - first_sys
91
  if bp_rise >= 20:
92
+ return AlertTier.AMBER, (
93
  f"SystolicBP rose {bp_rise:.0f} mmHg across visits "
94
+ f"-- escalation pattern detected"
95
  )
96
 
97
  return None, None
 
120
 
121
  # Determine tier
122
  if high_risk_prob >= RED_THRESHOLD:
123
+ # If very high confidence, default to RED
124
+ tier = AlertTier.RED
 
 
 
125
  elif high_risk_prob >= AMBER_THRESHOLD:
126
  tier = AlertTier.AMBER
127
  else:
api/extract_report.py CHANGED
@@ -340,32 +340,52 @@ def is_skip_line(line: str) -> bool:
340
 
341
 
342
  def parse_table_format(raw_text: str) -> List[dict]:
343
- """Parser A — Row-per-field table layout."""
 
 
 
344
  lines = [l.strip() for l in raw_text.split('\n') if l.strip()]
345
  field_vals = {}
346
  max_visits = 0
347
 
348
- for line in lines:
349
  if is_skip_line(line):
350
  continue
 
351
  field = match_label(line)
352
  if field is None:
353
  continue
354
- numbers = find_numbers(line)
 
 
 
 
 
 
355
  lo, hi = RANGES[field]
356
  valid = [n for n in numbers if lo <= n <= hi]
 
357
  if valid:
358
- field_vals[field] = valid
359
- max_visits = max(max_visits, len(valid))
 
 
 
 
360
 
361
  if not field_vals:
362
  return []
363
 
364
- return [
365
- {field: (values[i] if i < len(values) else None)
366
- for field, values in field_vals.items()}
367
- for i in range(max_visits)
368
- ]
 
 
 
 
 
369
 
370
 
371
  def parse_column_format(raw_text: str) -> List[dict]:
 
340
 
341
 
342
  def parse_table_format(raw_text: str) -> List[dict]:
343
+ """
344
+ Parser A -- Row-per-field table layout.
345
+ Upgraded to look at subsequent lines if the label line is empty.
346
+ """
347
  lines = [l.strip() for l in raw_text.split('\n') if l.strip()]
348
  field_vals = {}
349
  max_visits = 0
350
 
351
+ for idx, line in enumerate(lines):
352
  if is_skip_line(line):
353
  continue
354
+
355
  field = match_label(line)
356
  if field is None:
357
  continue
358
+
359
+ # Look for numbers in current line, and if none, the next 2 lines
360
+ search_chunk = line
361
+ if idx + 1 < len(lines): search_chunk += " " + lines[idx + 1]
362
+ if idx + 2 < len(lines): search_chunk += " " + lines[idx + 2]
363
+
364
+ numbers = find_numbers(search_chunk)
365
  lo, hi = RANGES[field]
366
  valid = [n for n in numbers if lo <= n <= hi]
367
+
368
  if valid:
369
+ # If we already have values for this field, append or merge
370
+ if field in field_vals:
371
+ field_vals[field].extend(valid)
372
+ else:
373
+ field_vals[field] = valid
374
+ max_visits = max(max_visits, len(field_vals[field]))
375
 
376
  if not field_vals:
377
  return []
378
 
379
+ # Assemble into visits
380
+ results = []
381
+ for i in range(max_visits):
382
+ visit = {}
383
+ for field in RANGES.keys():
384
+ vals = field_vals.get(field, [])
385
+ visit[field] = vals[i] if i < len(vals) else None
386
+ results.append(visit)
387
+
388
+ return results
389
 
390
 
391
  def parse_column_format(raw_text: str) -> List[dict]:
api/main.py CHANGED
@@ -160,10 +160,10 @@ async def predict(request: PredictionRequest):
160
  )
161
 
162
  # Override with clinical rule minimum if needed
163
- tier_order = {"GREEN": 0, "AMBER": 1, "RED": 2}
164
  if forced_tier is not None:
165
- if tier_order[forced_tier] > tier_order[alert_tier.value]:
166
- alert_tier = AlertTier(forced_tier)
167
  suppressed = False
168
 
169
  # Action text
 
160
  )
161
 
162
  # Override with clinical rule minimum if needed
163
+ tier_order = {AlertTier.GREEN: 0, AlertTier.AMBER: 1, AlertTier.RED: 2}
164
  if forced_tier is not None:
165
+ if tier_order[forced_tier] > tier_order[alert_tier]:
166
+ alert_tier = forced_tier
167
  suppressed = False
168
 
169
  # Action text
dashboard/app.js CHANGED
@@ -175,6 +175,7 @@ async function extractFromImage() {
175
  }
176
 
177
  const data = await resp.json();
 
178
 
179
  if (data.patient_id) {
180
  const pidField = document.getElementById("patient_id");
@@ -184,6 +185,7 @@ async function extractFromImage() {
184
  }
185
  }
186
 
 
187
  document.getElementById("visits-container").innerHTML = "";
188
  visitCount = 0;
189
 
@@ -202,12 +204,12 @@ async function extractFromImage() {
202
  <span style="color:${confColor};font-weight:600;">
203
  Extraction confidence: ${confPct}%
204
  </span>
205
- ${confPct < 80 ? " -- please verify highlighted fields before submitting" : " -- all fields extracted successfully"}
206
- ${data.notes ? `<br><span style="color:var(--text-muted);">${data.notes}</span>` : ""}
207
  `;
208
 
209
  statusEl.className = "extract-status success";
210
- statusEl.textContent = `Extracted ${visits.length} visit${visits.length > 1 ? "s" : ""} -- highlighted fields are auto-filled. Verify before submitting.`;
211
 
212
  } catch (err) {
213
  statusEl.className = "extract-status error";
@@ -284,9 +286,9 @@ function showResult(data) {
284
  const tierClass = { GREEN: "alert-green", AMBER: "alert-amber", RED: "alert-red" };
285
  const badgeClass = { GREEN: "badge-green", AMBER: "badge-amber", RED: "badge-red" };
286
  const tierLabel = {
287
- GREEN: "Low risk",
288
- AMBER: "Medium risk -- monitor closely",
289
- RED: "High risk -- refer immediately"
290
  };
291
 
292
  const card = document.getElementById("result-card");
 
175
  }
176
 
177
  const data = await resp.json();
178
+ console.log("Extraction successful:", data);
179
 
180
  if (data.patient_id) {
181
  const pidField = document.getElementById("patient_id");
 
185
  }
186
  }
187
 
188
+ // Clear existing visits
189
  document.getElementById("visits-container").innerHTML = "";
190
  visitCount = 0;
191
 
 
204
  <span style="color:${confColor};font-weight:600;">
205
  Extraction confidence: ${confPct}%
206
  </span>
207
+ ${confPct < 80 ? " -- verify highlighted fields" : " -- extraction successful"}
208
+ ${data.notes ? `<br><small>${data.notes}</small>` : ""}
209
  `;
210
 
211
  statusEl.className = "extract-status success";
212
+ statusEl.textContent = `Extracted ${visits.length} visit${visits.length > 1 ? "s" : ""}. Highlighted fields are auto-filled.`;
213
 
214
  } catch (err) {
215
  statusEl.className = "extract-status error";
 
286
  const tierClass = { GREEN: "alert-green", AMBER: "alert-amber", RED: "alert-red" };
287
  const badgeClass = { GREEN: "badge-green", AMBER: "badge-amber", RED: "badge-red" };
288
  const tierLabel = {
289
+ GREEN: "Normal / Low risk",
290
+ AMBER: "Elevated risk detected",
291
+ RED: "Critical risk -- REFER IMMEDIATELY"
292
  };
293
 
294
  const card = document.getElementById("result-card");