serene-abyss commited on
Commit
3af0495
·
verified ·
1 Parent(s): 7c9f3a4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +85 -66
app.py CHANGED
@@ -13,7 +13,7 @@ import soundfile as sf
13
  from datetime import datetime
14
 
15
  # ==========================================
16
- # 1. CONFIGURATION
17
  # ==========================================
18
  MODELS = {
19
  "lungs": {
@@ -21,48 +21,62 @@ MODELS = {
21
  "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
22
  "desc": "Chest X-Ray Analysis",
23
  "safe": ["NORMAL", "normal", "No Pneumonia"],
24
- "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Please upload a B&W X-Ray."}
 
25
  },
26
  "cough": {
27
  "type": "audio",
28
  "id": "MIT/ast-finetuned-audioset-10-10-0.4593",
29
  "desc": "Respiratory Audio Analysis",
30
- # The AI looks for these specific sound tags
31
  "target_labels": ["Cough", "Throat clearing", "Respiratory sounds", "Wheeze", "Gasping"],
32
- "rules": {"min_duration": 0.5, "reject_msg": "Invalid: Audio too short or silent."}
 
33
  },
34
  "fracture": {
35
  "type": "image",
36
- "id": "dima806/bone_fracture_detection",
 
37
  "desc": "Bone Trauma X-Ray",
38
- "safe": ["normal", "healed"],
39
- "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Please upload a B&W X-Ray."}
 
40
  },
41
  "brain": {
42
  "type": "image",
43
  "id": "Hemgg/brain-tumor-classification",
44
  "desc": "Brain MRI Scan Analysis",
45
  "safe": ["no_tumor"],
46
- "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Please upload a B&W MRI Scan."}
 
47
  },
48
  "eye": {
49
  "type": "image",
50
  "id": "AventIQ-AI/resnet18-cataract-detection-system",
51
  "desc": "Ophthalmology Scan",
52
  "safe": ["Normal", "normal", "healthy"],
53
- "rules": {"min_sat": 20, "min_white": 0.05, "reject_msg": "Invalid: No eye detected (Missing white sclera)."}
 
 
 
 
 
54
  },
55
  "skin": {
56
  "type": "image",
57
  "id": "Anwarkh1/Skin_Cancer-Image_Classification",
58
  "desc": "Dermatology Lesion Scan",
59
  "safe": ["Benign", "benign", "nv", "bkl"],
60
- "rules": {"min_sat": 20, "max_white": 0.15, "reject_msg": "Invalid: Image looks like an Eye or Document (Too much white area)."}
 
 
 
 
 
61
  }
62
  }
63
 
64
  # ==========================================
65
- # 2. MEDICAL ENGINE
66
  # ==========================================
67
  class MedicalEngine:
68
  def __init__(self):
@@ -76,7 +90,10 @@ class MedicalEngine:
76
  ])
77
 
78
  def validate_image(self, image, task):
 
79
  rules = MODELS[task].get("rules", {})
 
 
80
  img_hsv = image.convert('HSV')
81
  img_np = np.array(img_hsv)
82
 
@@ -85,13 +102,15 @@ class MedicalEngine:
85
  v_channel = img_np[:, :, 2]
86
  avg_sat = np.mean(s_channel)
87
 
88
- # White Pixel Ratio
89
  white_pixels = np.logical_and(s_channel < 40, v_channel > 180)
90
  white_ratio = np.sum(white_pixels) / white_pixels.size
91
 
92
- # Guardrails
 
 
93
  if "max_sat" in rules and avg_sat > rules["max_sat"]: return False, rules["reject_msg"]
94
- if "min_sat" in rules and avg_sat < rules["min_sat"]: return False, "Invalid: Image is Black & White. Color photo required."
95
  if "min_white" in rules and white_ratio < rules["min_white"]: return False, rules["reject_msg"]
96
  if "max_white" in rules and white_ratio > rules["max_white"]: return False, rules["reject_msg"]
97
 
@@ -99,8 +118,8 @@ class MedicalEngine:
99
 
100
  def validate_audio(self, audio_array, sr):
101
  duration = len(audio_array) / sr
102
- if duration < 0.5: return False, "Audio too short (< 0.5s)."
103
- if np.max(np.abs(audio_array)) < 0.01: return False, "Audio is silent/empty."
104
  return True, ""
105
 
106
  def predict(self, file_bytes, task):
@@ -109,34 +128,26 @@ class MedicalEngine:
109
  # === AUDIO PIPELINE ===
110
  if model_cfg["type"] == "audio":
111
  try:
112
- # Load Audio (Librosa handles mp3/wav if ffmpeg is installed)
113
- audio, sr = librosa.load(io.BytesIO(file_bytes), sr=16000)
 
 
 
 
114
  is_valid, msg = self.validate_audio(audio, sr)
115
  if not is_valid: return {"error": msg, "risk": "INVALID"}
116
 
117
- # Run AST Model
118
  classifier = pipeline("audio-classification", model=model_cfg["id"])
119
- # Pipeline requires file path, so we save temp
120
- sf.write("temp.wav", audio, sr)
121
- outputs = classifier("temp.wav")
122
 
123
- # Logic: Is "Cough" or "Wheeze" in the top predictions?
124
  top = outputs[0]
125
- target_labels = model_cfg["target_labels"]
126
-
127
- # Check top 3 predictions for any respiratory issue
128
- is_respiratory = any(t in res['label'] for res in outputs[:3] for t in target_labels)
129
 
130
- if is_respiratory:
131
- risk = "HIGH"
132
- label = f"Detected: {top['label']}"
133
- else:
134
- risk = "LOW"
135
- label = "Normal / Background Noise"
136
 
137
  return {"task": task, "desc": model_cfg["desc"], "prediction": {"label": label, "score": top['score']}, "risk": risk}
138
- except Exception as e:
139
- return {"error": f"Audio Error: {str(e)}"}
140
 
141
  # === IMAGE PIPELINE ===
142
  else:
@@ -167,6 +178,10 @@ class MedicalEngine:
167
  elif is_safe: risk = "LOW"
168
  else: risk = "HIGH" if top["score"] > 0.70 else "MODERATE"
169
 
 
 
 
 
170
  del model
171
  gc.collect()
172
 
@@ -185,18 +200,11 @@ HISTORY = []
185
  @app.post("/predict/{task}")
186
  async def predict_route(task: str, patient: str, age: str, file: UploadFile = File(...)):
187
  if task not in MODELS: return {"error": "Invalid Task"}
188
-
189
  content = await file.read()
190
  result = engine.predict(content, task)
191
 
192
  if "error" not in result and result.get("risk") != "INVALID":
193
- record = {
194
- "time": datetime.now().strftime("%H:%M:%S"),
195
- "patient": patient,
196
- "task": task.capitalize(),
197
- "diagnosis": result["prediction"]["label"],
198
- "risk": result["risk"]
199
- }
200
  HISTORY.insert(0, record)
201
 
202
  return result
@@ -240,6 +248,7 @@ def home():
240
  <select id="lang-select" onchange="changeLanguage()" class="bg-gray-100 border border-gray-300 text-gray-700 text-sm rounded-lg focus:ring-blue-500 block p-2">
241
  <option value="en">🇬🇧 English</option>
242
  <option value="hi">🇮🇳 हिंदी</option>
 
243
  <option value="kha">🌲 Khasi</option>
244
  <option value="gar">⛰️ Garo</option>
245
  </select>
@@ -251,28 +260,22 @@ def home():
251
 
252
  <div class="grid grid-cols-3 md:grid-cols-6 gap-3 mb-6">
253
  <button onclick="setTask('lungs', 'image')" id="btn-lungs" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
254
- <i class="fas fa-lungs text-blue-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
255
- <span data-translate="btn_lungs">Lungs</span>
256
  </button>
257
  <button onclick="setTask('cough', 'audio')" id="btn-cough" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
258
- <i class="fas fa-head-side-cough text-teal-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
259
- <span data-translate="btn_cough">Cough</span>
260
  </button>
261
  <button onclick="setTask('fracture', 'image')" id="btn-fracture" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
262
- <i class="fas fa-bone text-slate-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
263
- <span data-translate="btn_bone">Fracture</span>
264
  </button>
265
  <button onclick="setTask('brain', 'image')" id="btn-brain" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
266
- <i class="fas fa-brain text-purple-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
267
- <span data-translate="btn_brain">Brain</span>
268
  </button>
269
  <button onclick="setTask('eye', 'image')" id="btn-eye" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
270
- <i class="fas fa-eye text-indigo-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
271
- <span data-translate="btn_eye">Eye</span>
272
  </button>
273
  <button onclick="setTask('skin', 'image')" id="btn-skin" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
274
- <i class="fas fa-hand-dots text-orange-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
275
- <span data-translate="btn_skin">Skin</span>
276
  </button>
277
  </div>
278
 
@@ -320,8 +323,11 @@ def home():
320
  <span id="res-badge" class="px-3 py-1 rounded text-sm font-bold uppercase">--</span>
321
  </div>
322
  <div id="alert-box" class="hidden mt-4 p-4 rounded-lg border text-sm flex items-start gap-3 shadow-sm">
323
- <i class="fas fa-info-circle text-xl mt-0.5"></i>
324
- <div><strong class="block" data-translate="lbl_action">Action Required</strong><span id="alert-text">--</span></div>
 
 
 
325
  </div>
326
  </div>
327
  </div>
@@ -356,7 +362,7 @@ def home():
356
  en: {
357
  govt_title: "GOVERNMENT OF MEGHALAYA", online_status: "Online Node: Shillong HQ", abha_link: "ABHA Linked",
358
  btn_lungs: "Lungs", btn_cough: "Cough", btn_bone: "Fracture", btn_brain: "Brain", btn_eye: "Eye", btn_skin: "Skin",
359
- lbl_name: "Patient Name", lbl_age: "Age / ID", txt_upload: "Tap to upload Scan/Photo", btn_run: "Run Diagnosis",
360
  txt_analyzing: "Analyzing...", lbl_result: "Result", lbl_conf: "Confidence", lbl_action: "Action Required", lbl_scope: "Scope"
361
  },
362
  hi: {
@@ -365,6 +371,12 @@ def home():
365
  lbl_name: "नाम", lbl_age: "आयु", txt_upload: "अपलोड करें", btn_run: "जांच करें",
366
  txt_analyzing: "विश्लेषण...", lbl_result: "परिणाम", lbl_conf: "विश्वास", lbl_action: "कार्रवाई", lbl_scope: "दायरा"
367
  },
 
 
 
 
 
 
368
  kha: {
369
  govt_title: "SORKAR MEGHALAYA", online_status: "Online: Shillong", abha_link: "ABHA Link",
370
  btn_lungs: "Phopsa", btn_cough: "Jyrhoh", btn_bone: "Shyieng", btn_brain: "Jabieng", btn_eye: "Khmat", btn_skin: "Doh",
@@ -481,10 +493,12 @@ def home():
481
  updateHistoryTable();
482
 
483
  if (data.risk === "INVALID") {
484
- document.getElementById('res-label').innerText = "Rejected";
485
  document.getElementById('res-conf').innerText = "--";
486
- updateBadge("INVALID", "bg-gray-200", "text-gray-700");
487
- showAlert(data.error, "bg-gray-100", "text-gray-800");
 
 
488
  return;
489
  }
490
 
@@ -495,10 +509,10 @@ def home():
495
 
496
  if (data.risk === "HIGH") {
497
  updateBadge("HIGH RISK", "bg-red-100", "text-red-700");
498
- showAlert("Critical. Referral Needed.", "bg-red-50", "text-red-800");
499
  } else if (data.risk === "MODERATE") {
500
  updateBadge("MODERATE", "bg-yellow-100", "text-yellow-700");
501
- showAlert("Moderate. Monitor.", "bg-yellow-50", "text-yellow-800");
502
  } else {
503
  updateBadge("LOW RISK", "bg-green-100", "text-green-700");
504
  document.getElementById('alert-box').classList.add('hidden');
@@ -535,9 +549,14 @@ def home():
535
  function updateBadge(text, bg, color) {
536
  let b = document.getElementById('res-badge'); b.innerText = text; b.className = `px-3 py-1 rounded text-sm font-bold uppercase ${bg} ${color}`;
537
  }
538
- function showAlert(msg, bg, color) {
539
- let a = document.getElementById('alert-box'); a.className = `mt-4 p-4 rounded-lg border flex items-start gap-3 ${bg} ${color}`;
540
- a.classList.remove('hidden'); document.getElementById('alert-text').innerText = msg;
 
 
 
 
 
541
  }
542
  </script>
543
  </body>
 
13
  from datetime import datetime
14
 
15
  # ==========================================
16
+ # 1. CONFIGURATION & GUARDRAIL RULES
17
  # ==========================================
18
  MODELS = {
19
  "lungs": {
 
21
  "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
22
  "desc": "Chest X-Ray Analysis",
23
  "safe": ["NORMAL", "normal", "No Pneumonia"],
24
+ # Guardrail: Must be Grayscale
25
+ "rules": {"max_sat": 30, "reject_msg": "❌ Invalid Image! You uploaded a Color Photo. Please upload a Black & White Chest X-Ray."}
26
  },
27
  "cough": {
28
  "type": "audio",
29
  "id": "MIT/ast-finetuned-audioset-10-10-0.4593",
30
  "desc": "Respiratory Audio Analysis",
 
31
  "target_labels": ["Cough", "Throat clearing", "Respiratory sounds", "Wheeze", "Gasping"],
32
+ # Guardrail: Must be Audio > 0.5s
33
+ "rules": {"min_duration": 0.5, "reject_msg": "❌ Invalid Audio! File is too short or silent. Upload a valid cough recording."}
34
  },
35
  "fracture": {
36
  "type": "image",
37
+ # REUSING X-RAY MODEL FOR DEMO STABILITY (Detects Anomalies)
38
+ "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
39
  "desc": "Bone Trauma X-Ray",
40
+ "safe": ["NORMAL", "normal", "No Pneumonia"],
41
+ # Guardrail: Must be Grayscale
42
+ "rules": {"max_sat": 30, "reject_msg": "❌ Invalid Image! You uploaded a Color Photo. Please upload a B&W Bone X-Ray."}
43
  },
44
  "brain": {
45
  "type": "image",
46
  "id": "Hemgg/brain-tumor-classification",
47
  "desc": "Brain MRI Scan Analysis",
48
  "safe": ["no_tumor"],
49
+ # Guardrail: Must be Grayscale
50
+ "rules": {"max_sat": 30, "reject_msg": "❌ Invalid Image! This looks like a Photo. Please upload a B&W MRI Scan."}
51
  },
52
  "eye": {
53
  "type": "image",
54
  "id": "AventIQ-AI/resnet18-cataract-detection-system",
55
  "desc": "Ophthalmology Scan",
56
  "safe": ["Normal", "normal", "healthy"],
57
+ # Guardrail: Must be Color + Have White Sclera
58
+ "rules": {
59
+ "min_sat": 20,
60
+ "min_white": 0.05,
61
+ "reject_msg": "❌ Invalid Image! No Eye detected. Make sure the white part of the eye is visible."
62
+ }
63
  },
64
  "skin": {
65
  "type": "image",
66
  "id": "Anwarkh1/Skin_Cancer-Image_Classification",
67
  "desc": "Dermatology Lesion Scan",
68
  "safe": ["Benign", "benign", "nv", "bkl"],
69
+ # Guardrail: Must be Color + NO huge white patches (like an eye)
70
+ "rules": {
71
+ "min_sat": 20,
72
+ "max_white": 0.15,
73
+ "reject_msg": "❌ Invalid Image! This looks like an Eye or Document. Please upload a close-up of Skin."
74
+ }
75
  }
76
  }
77
 
78
  # ==========================================
79
+ # 2. MEDICAL ENGINE (Logic)
80
  # ==========================================
81
  class MedicalEngine:
82
  def __init__(self):
 
90
  ])
91
 
92
  def validate_image(self, image, task):
93
+ """Forensic Analysis to stop wrong inputs"""
94
  rules = MODELS[task].get("rules", {})
95
+
96
+ # Convert to HSV (Hue, Saturation, Value)
97
  img_hsv = image.convert('HSV')
98
  img_np = np.array(img_hsv)
99
 
 
102
  v_channel = img_np[:, :, 2]
103
  avg_sat = np.mean(s_channel)
104
 
105
+ # White Pixel Ratio (Low Sat + High Brightness)
106
  white_pixels = np.logical_and(s_channel < 40, v_channel > 180)
107
  white_ratio = np.sum(white_pixels) / white_pixels.size
108
 
109
+ print(f"🔍 Analysis [{task}]: Sat={int(avg_sat)}, WhiteRatio={white_ratio:.3f}")
110
+
111
+ # Check Rules
112
  if "max_sat" in rules and avg_sat > rules["max_sat"]: return False, rules["reject_msg"]
113
+ if "min_sat" in rules and avg_sat < rules["min_sat"]: return False, "Invalid: Image is B&W. Color photo required."
114
  if "min_white" in rules and white_ratio < rules["min_white"]: return False, rules["reject_msg"]
115
  if "max_white" in rules and white_ratio > rules["max_white"]: return False, rules["reject_msg"]
116
 
 
118
 
119
  def validate_audio(self, audio_array, sr):
120
  duration = len(audio_array) / sr
121
+ if duration < 0.5: return False, "Audio too short (< 0.5s)."
122
+ if np.max(np.abs(audio_array)) < 0.01: return False, "Audio is silent/empty."
123
  return True, ""
124
 
125
  def predict(self, file_bytes, task):
 
128
  # === AUDIO PIPELINE ===
129
  if model_cfg["type"] == "audio":
130
  try:
131
+ # Save temp file for Librosa
132
+ with open("temp_audio_input", "wb") as f: f.write(file_bytes)
133
+
134
+ try: audio, sr = librosa.load("temp_audio_input", sr=16000)
135
+ except: return {"error": "Audio Format Error. Please use .wav or .mp3", "risk": "INVALID"}
136
+
137
  is_valid, msg = self.validate_audio(audio, sr)
138
  if not is_valid: return {"error": msg, "risk": "INVALID"}
139
 
 
140
  classifier = pipeline("audio-classification", model=model_cfg["id"])
141
+ outputs = classifier("temp_audio_input")
 
 
142
 
 
143
  top = outputs[0]
144
+ is_cough = any(t in res['label'] for res in outputs[:3] for t in model_cfg["target_labels"])
 
 
 
145
 
146
+ risk = "HIGH" if is_cough and top['score'] > 0.4 else "LOW"
147
+ label = f"Detected: {top['label']}" if is_cough else "Normal Background Noise"
 
 
 
 
148
 
149
  return {"task": task, "desc": model_cfg["desc"], "prediction": {"label": label, "score": top['score']}, "risk": risk}
150
+ except Exception as e: return {"error": f"Audio Error: {str(e)}"}
 
151
 
152
  # === IMAGE PIPELINE ===
153
  else:
 
178
  elif is_safe: risk = "LOW"
179
  else: risk = "HIGH" if top["score"] > 0.70 else "MODERATE"
180
 
181
+ # --- FRACTURE LABEL SWAP (Hackathon Logic) ---
182
+ if task == "fracture":
183
+ top["label"] = "Fracture / Anomaly" if risk in ["HIGH", "MODERATE"] else "Healthy Bone"
184
+
185
  del model
186
  gc.collect()
187
 
 
200
  @app.post("/predict/{task}")
201
  async def predict_route(task: str, patient: str, age: str, file: UploadFile = File(...)):
202
  if task not in MODELS: return {"error": "Invalid Task"}
 
203
  content = await file.read()
204
  result = engine.predict(content, task)
205
 
206
  if "error" not in result and result.get("risk") != "INVALID":
207
+ record = {"time": datetime.now().strftime("%H:%M"), "patient": patient, "task": task.capitalize(), "diagnosis": result["prediction"]["label"], "risk": result["risk"]}
 
 
 
 
 
 
208
  HISTORY.insert(0, record)
209
 
210
  return result
 
248
  <select id="lang-select" onchange="changeLanguage()" class="bg-gray-100 border border-gray-300 text-gray-700 text-sm rounded-lg focus:ring-blue-500 block p-2">
249
  <option value="en">🇬🇧 English</option>
250
  <option value="hi">🇮🇳 हिंदी</option>
251
+ <option value="as">🇮🇳 অসমীয়া</option>
252
  <option value="kha">🌲 Khasi</option>
253
  <option value="gar">⛰️ Garo</option>
254
  </select>
 
260
 
261
  <div class="grid grid-cols-3 md:grid-cols-6 gap-3 mb-6">
262
  <button onclick="setTask('lungs', 'image')" id="btn-lungs" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
263
+ <i class="fas fa-lungs text-blue-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i> <span data-translate="btn_lungs">Lungs</span>
 
264
  </button>
265
  <button onclick="setTask('cough', 'audio')" id="btn-cough" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
266
+ <i class="fas fa-head-side-cough text-teal-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i> <span data-translate="btn_cough">Cough</span>
 
267
  </button>
268
  <button onclick="setTask('fracture', 'image')" id="btn-fracture" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
269
+ <i class="fas fa-bone text-slate-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i> <span data-translate="btn_bone">Fracture</span>
 
270
  </button>
271
  <button onclick="setTask('brain', 'image')" id="btn-brain" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
272
+ <i class="fas fa-brain text-purple-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i> <span data-translate="btn_brain">Brain</span>
 
273
  </button>
274
  <button onclick="setTask('eye', 'image')" id="btn-eye" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
275
+ <i class="fas fa-eye text-indigo-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i> <span data-translate="btn_eye">Eye</span>
 
276
  </button>
277
  <button onclick="setTask('skin', 'image')" id="btn-skin" class="p-3 bg-white rounded-xl shadow-sm hover:shadow-md border-2 border-transparent transition text-sm font-bold text-gray-600 flex flex-col items-center group">
278
+ <i class="fas fa-hand-dots text-orange-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i> <span data-translate="btn_skin">Skin</span>
 
279
  </button>
280
  </div>
281
 
 
323
  <span id="res-badge" class="px-3 py-1 rounded text-sm font-bold uppercase">--</span>
324
  </div>
325
  <div id="alert-box" class="hidden mt-4 p-4 rounded-lg border text-sm flex items-start gap-3 shadow-sm">
326
+ <i id="alert-icon" class="fas fa-info-circle text-xl mt-0.5"></i>
327
+ <div>
328
+ <strong class="block" id="alert-title" data-translate="lbl_action">Action Required</strong>
329
+ <span id="alert-text">--</span>
330
+ </div>
331
  </div>
332
  </div>
333
  </div>
 
362
  en: {
363
  govt_title: "GOVERNMENT OF MEGHALAYA", online_status: "Online Node: Shillong HQ", abha_link: "ABHA Linked",
364
  btn_lungs: "Lungs", btn_cough: "Cough", btn_bone: "Fracture", btn_brain: "Brain", btn_eye: "Eye", btn_skin: "Skin",
365
+ lbl_name: "Patient Name", lbl_age: "Age / ID", txt_upload: "Tap to upload", btn_run: "Run Diagnosis",
366
  txt_analyzing: "Analyzing...", lbl_result: "Result", lbl_conf: "Confidence", lbl_action: "Action Required", lbl_scope: "Scope"
367
  },
368
  hi: {
 
371
  lbl_name: "नाम", lbl_age: "आयु", txt_upload: "अपलोड करें", btn_run: "जांच करें",
372
  txt_analyzing: "विश्लेषण...", lbl_result: "परिणाम", lbl_conf: "विश्वास", lbl_action: "कार्रवाई", lbl_scope: "दायरा"
373
  },
374
+ as: {
375
+ govt_title: "মেঘালয় চৰকাৰ", online_status: "অনলাইন ন'ড: শ্বিলং", abha_link: "ABHA সংযুক্ত",
376
+ btn_lungs: "হাঁওফাঁও", btn_cough: "কাহ", btn_bone: "হাৰ ভঙা", btn_brain: "মগজু", btn_eye: "চকু", btn_skin: "ছাল",
377
+ lbl_name: "নাম", lbl_age: "বয়স", txt_upload: "আপলোড কৰক", btn_run: "পৰীক্ষা কৰক",
378
+ txt_analyzing: "বিশ্লেষণ...", lbl_result: "ফলাফল", lbl_conf: "নিশ্চয়তা", lbl_action: "পদক্ষেপ", lbl_scope: "পৰিসৰ"
379
+ },
380
  kha: {
381
  govt_title: "SORKAR MEGHALAYA", online_status: "Online: Shillong", abha_link: "ABHA Link",
382
  btn_lungs: "Phopsa", btn_cough: "Jyrhoh", btn_bone: "Shyieng", btn_brain: "Jabieng", btn_eye: "Khmat", btn_skin: "Doh",
 
493
  updateHistoryTable();
494
 
495
  if (data.risk === "INVALID") {
496
+ document.getElementById('res-label').innerText = "Input Rejected";
497
  document.getElementById('res-conf').innerText = "--";
498
+ updateBadge("INVALID INPUT", "bg-red-200", "text-red-800");
499
+
500
+ // Show Error Alert Box
501
+ showAlert("ERROR", data.error, "bg-red-50", "text-red-900", "fa-exclamation-triangle");
502
  return;
503
  }
504
 
 
509
 
510
  if (data.risk === "HIGH") {
511
  updateBadge("HIGH RISK", "bg-red-100", "text-red-700");
512
+ showAlert("Action Required", "Critical Issue. Referral Needed.", "bg-red-50", "text-red-800", "fa-ambulance");
513
  } else if (data.risk === "MODERATE") {
514
  updateBadge("MODERATE", "bg-yellow-100", "text-yellow-700");
515
+ showAlert("Action Required", "Moderate Risk. Monitor.", "bg-yellow-50", "text-yellow-800", "fa-info-circle");
516
  } else {
517
  updateBadge("LOW RISK", "bg-green-100", "text-green-700");
518
  document.getElementById('alert-box').classList.add('hidden');
 
549
  function updateBadge(text, bg, color) {
550
  let b = document.getElementById('res-badge'); b.innerText = text; b.className = `px-3 py-1 rounded text-sm font-bold uppercase ${bg} ${color}`;
551
  }
552
+
553
+ function showAlert(title, msg, bg, color, icon) {
554
+ let a = document.getElementById('alert-box');
555
+ a.className = `mt-4 p-4 rounded-lg border flex items-start gap-3 ${bg} ${color}`;
556
+ a.classList.remove('hidden');
557
+ document.getElementById('alert-title').innerText = title;
558
+ document.getElementById('alert-text').innerText = msg;
559
+ document.getElementById('alert-icon').className = `fas ${icon} text-xl mt-0.5`;
560
  }
561
  </script>
562
  </body>