serene-abyss commited on
Commit
a94e53c
·
verified ·
1 Parent(s): a3e2d51

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +198 -85
app.py CHANGED
@@ -20,28 +20,42 @@ MODELS = {
20
  "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
21
  "desc": "Tuberculosis & Pneumonia (Chest X-Ray)",
22
  "safe": ["NORMAL", "normal", "No Pneumonia"],
23
- "rules": {"max_saturation": 30, "reject_msg": "Invalid: Too colorful. Upload B&W X-Ray."}
24
  },
25
  "cough": {
26
  "type": "audio",
27
  "id": "MIT/ast-finetuned-audioset-10-10-0.4593",
28
  "desc": "COPD & Respiratory Screening (Cough Audio)",
29
  "target_labels": ["Cough", "Throat clearing", "Respiratory sounds", "Wheeze"],
30
- "rules": {"min_duration": 1.0, "reject_msg": "Invalid: Audio too short or silent."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  },
32
  "eye": {
33
  "type": "image",
34
  "id": "AventIQ-AI/resnet18-cataract-detection-system",
35
  "desc": "Cataract Detection (Eye Photo)",
36
  "safe": ["Normal", "normal", "healthy"],
37
- "rules": {"min_saturation": 20, "min_white_ratio": 0.05, "reject_msg": "Invalid: No eye detected."}
38
  },
39
  "skin": {
40
  "type": "image",
41
  "id": "Anwarkh1/Skin_Cancer-Image_Classification",
42
  "desc": "Dermatology Analysis (Lesion Photo)",
43
  "safe": ["Benign", "benign", "nv", "bkl"],
44
- "rules": {"min_saturation": 20, "max_white_ratio": 0.25, "reject_msg": "Invalid: Not a skin close-up."}
45
  }
46
  }
47
 
@@ -59,7 +73,6 @@ class MedicalEngine:
59
  transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
60
  ])
61
 
62
- # --- IMAGE VALIDATION ---
63
  def validate_image(self, image, task):
64
  rules = MODELS[task].get("rules", {})
65
  img_hsv = image.convert('HSV')
@@ -71,63 +84,44 @@ class MedicalEngine:
71
  white_pixels = np.logical_and(img_np[:,:,1] < 40, img_np[:,:,2] > 180)
72
  white_ratio = np.sum(white_pixels) / white_pixels.size
73
 
74
- if "max_saturation" in rules and avg_sat > rules["max_saturation"]: return False, rules["reject_msg"]
75
- if "min_saturation" in rules and avg_sat < rules["min_saturation"]: return False, rules["reject_msg"]
76
- if "min_white_ratio" in rules and white_ratio < rules["min_white_ratio"]: return False, rules["reject_msg"]
77
- if "max_white_ratio" in rules and white_ratio > rules["max_white_ratio"]: return False, rules["reject_msg"]
78
 
79
  return True, ""
80
 
81
- # --- AUDIO VALIDATION ---
82
  def validate_audio(self, audio_array, sr):
83
  duration = len(audio_array) / sr
84
  if duration < 0.5: return False, "Audio too short (< 0.5s)."
85
  if np.max(np.abs(audio_array)) < 0.01: return False, "Audio is silent/empty."
86
  return True, ""
87
 
88
- # --- PREDICTION LOGIC ---
89
  def predict(self, file_bytes, task):
90
  model_cfg = MODELS[task]
91
 
92
- # === AUDIO PIPELINE ===
93
  if model_cfg["type"] == "audio":
94
- print(f"⏳ Processing Audio for {task}...")
95
  try:
96
- # Load Audio
97
  audio, sr = librosa.load(io.BytesIO(file_bytes), sr=16000)
98
-
99
- # Guardrail
100
  is_valid, msg = self.validate_audio(audio, sr)
101
  if not is_valid: return {"error": msg, "risk": "INVALID"}
102
 
103
- # Inference
104
  classifier = pipeline("audio-classification", model=model_cfg["id"])
105
- # We save to temp file because pipeline prefers paths or structured input
106
  sf.write("temp.wav", audio, sr)
107
  outputs = classifier("temp.wav")
108
 
109
- # Logic: Check if top prediction is a "Cough"
110
  top = outputs[0]
111
  target_labels = model_cfg["target_labels"]
112
-
113
- # Check if ANY of the top 3 results match "Cough"
114
  is_cough = any(target in res['label'] for res in outputs[:3] for target in target_labels)
115
 
116
  risk = "HIGH" if is_cough and top['score'] > 0.4 else "LOW"
117
  label = f"Detected: {top['label']}" if is_cough else "Normal / Background Noise"
118
 
119
- return {
120
- "task": task,
121
- "desc": model_cfg["desc"],
122
- "prediction": {"label": label, "score": top['score']},
123
- "risk": risk
124
- }
125
  except Exception as e:
126
  return {"error": f"Audio Error: {str(e)}"}
127
 
128
- # === IMAGE PIPELINE ===
129
  else:
130
- print(f"⏳ Processing Image for {task}...")
131
  try:
132
  image = Image.open(io.BytesIO(file_bytes)).convert("RGB")
133
  is_valid, msg = self.validate_image(image, task)
@@ -138,7 +132,7 @@ class MedicalEngine:
138
  model.to(self.device)
139
  model.eval()
140
 
141
- inputs = self.transform(image).unsqueeze(0).to(self.device)
142
  with torch.no_grad():
143
  outputs = model(inputs)
144
  probs = F.softmax(outputs.logits, dim=-1)
@@ -152,7 +146,7 @@ class MedicalEngine:
152
 
153
  if top["score"] < 0.5: risk = "UNCERTAIN"
154
  elif is_safe: risk = "LOW"
155
- else: risk = "HIGH" if top["score"] > 0.75 else "MODERATE"
156
 
157
  del model
158
  gc.collect()
@@ -180,93 +174,130 @@ def home():
180
  <!DOCTYPE html>
181
  <html>
182
  <head>
183
- <title>MediScan Rural | Ayushman Bharat Node</title>
184
  <script src="https://cdn.tailwindcss.com"></script>
185
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 
186
  </head>
187
  <body class="bg-slate-50 min-h-screen flex flex-col font-sans">
188
 
189
- <nav class="bg-blue-900 text-white p-4 shadow-xl">
190
  <div class="container mx-auto flex justify-between items-center">
191
- <div class="flex items-center gap-3">
192
- <i class="fas fa-heartbeat text-2xl text-green-400"></i>
193
  <div>
194
- <h1 class="text-xl font-bold">MediScan Rural</h1>
195
- <p class="text-xs text-blue-200">North East Node • Online</p>
 
 
 
 
196
  </div>
197
  </div>
198
- <div class="text-right hidden md:block">
199
- <span class="bg-blue-800 px-3 py-1 rounded text-xs font-bold border border-blue-600">
200
- <i class="fas fa-satellite-dish mr-1"></i> Ayushman Bharat Linked
201
- </span>
 
 
 
 
 
 
 
 
 
 
202
  </div>
203
  </div>
204
  </nav>
205
 
206
- <div class="container mx-auto mt-8 p-4 max-w-3xl flex-grow">
207
 
208
- <div class="grid grid-cols-4 gap-2 mb-6">
209
- <button onclick="setTask('lungs', 'image')" id="btn-lungs" class="p-3 bg-white rounded-lg shadow hover:bg-blue-50 border-2 border-transparent transition text-sm font-bold text-gray-600">
210
- <i class="fas fa-lungs text-blue-500 block text-2xl mb-1"></i> Lungs
 
211
  </button>
212
- <button onclick="setTask('cough', 'audio')" id="btn-cough" class="p-3 bg-white rounded-lg shadow hover:bg-teal-50 border-2 border-transparent transition text-sm font-bold text-gray-600">
213
- <i class="fas fa-head-side-cough text-teal-500 block text-2xl mb-1"></i> COPD
 
214
  </button>
215
- <button onclick="setTask('eye', 'image')" id="btn-eye" class="p-3 bg-white rounded-lg shadow hover:bg-indigo-50 border-2 border-transparent transition text-sm font-bold text-gray-600">
216
- <i class="fas fa-eye text-indigo-500 block text-2xl mb-1"></i> Eye
 
217
  </button>
218
- <button onclick="setTask('skin', 'image')" id="btn-skin" class="p-3 bg-white rounded-lg shadow hover:bg-orange-50 border-2 border-transparent transition text-sm font-bold text-gray-600">
219
- <i class="fas fa-hand-dots text-orange-500 block text-2xl mb-1"></i> Skin
 
 
 
 
 
 
 
 
 
220
  </button>
221
  </div>
222
 
223
- <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200">
224
- <h2 id="header-text" class="text-xl font-bold text-gray-700 mb-4">Select a Category above</h2>
225
 
226
  <div id="inputs" class="opacity-50 pointer-events-none transition-opacity mb-6">
227
  <div class="grid grid-cols-2 gap-4 mb-4">
228
- <input type="text" id="p-name" placeholder="Patient Name" class="border p-2 rounded bg-gray-50">
229
- <input type="text" placeholder="Age / ID" class="border p-2 rounded bg-gray-50">
 
 
 
 
 
 
230
  </div>
231
 
232
- <div onclick="document.getElementById('file-input').click()" class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:bg-gray-50 transition">
233
  <input type="file" id="file-input" class="hidden" onchange="showPreview(event)" onclick="this.value=null">
234
 
235
- <div id="placeholder">
236
- <i id="upload-icon" class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-2"></i>
237
- <p id="upload-text" class="text-gray-500 text-sm">Tap to upload</p>
238
  </div>
239
 
240
- <img id="img-preview" class="hidden mx-auto max-h-48 rounded shadow object-contain">
241
  <audio id="audio-preview" controls class="hidden w-full mt-4"></audio>
242
  </div>
243
  </div>
244
 
245
- <button id="run-btn" onclick="analyze()" class="hidden w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-xl shadow-lg transition">
246
- <i class="fas fa-microscope mr-2"></i> Run Diagnosis
247
  </button>
248
 
249
  <div id="loader" class="hidden text-center py-6">
250
  <div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"></div>
251
- <p class="text-sm text-gray-500 mt-2 font-semibold">Analyzing Data...</p>
252
  </div>
253
 
254
- <div id="result-box" class="hidden mt-6 border-t pt-6">
255
  <div class="flex justify-between items-start">
256
  <div>
257
- <p class="text-xs font-bold text-gray-400 uppercase">Analysis</p>
258
  <h1 id="res-label" class="text-2xl font-extrabold text-gray-800">--</h1>
259
- <p class="text-sm text-gray-600 mt-1">Confidence: <span id="res-conf" class="font-mono font-bold">--</span></p>
260
  </div>
261
  <span id="res-badge" class="px-3 py-1 rounded text-sm font-bold uppercase">--</span>
262
  </div>
263
 
264
- <div id="alert-box" class="hidden mt-4 p-3 bg-red-50 text-red-800 rounded border border-red-200 text-sm">
265
- <i class="fas fa-ambulance mr-2"></i> <span id="alert-text">High Risk Detected.</span>
 
 
 
 
266
  </div>
267
 
268
- <div class="mt-4 bg-gray-50 p-3 rounded border flex items-center justify-between text-xs">
269
- <span class="font-bold text-gray-500"><i class="fas fa-database mr-1"></i> Govt Sync</span>
270
  <span id="sync-msg" class="text-yellow-600"><i class="fas fa-sync fa-spin"></i> Pending...</span>
271
  </div>
272
  </div>
@@ -275,38 +306,120 @@ def home():
275
  </div>
276
 
277
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  let currTask = null;
279
  let currType = 'image';
280
  let currFile = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
  function setTask(task, type) {
283
  currTask = task;
284
  currType = type;
 
285
 
286
- // Highlight Buttons
287
- document.querySelectorAll('button[id^="btn-"]').forEach(b => b.classList.remove('ring-2', 'ring-blue-400', 'border-blue-500'));
288
- document.getElementById('btn-'+task).classList.add('ring-2', 'ring-blue-400', 'border-blue-500');
 
289
 
290
- // UI Text
291
- let taskName = task === 'cough' ? 'COUGH Audio' : task.toUpperCase() + ' Image';
292
  document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${taskName}</span>`;
293
 
294
- // Input Type Handling
295
  let input = document.getElementById('file-input');
296
  let icon = document.getElementById('upload-icon');
297
  let text = document.getElementById('upload-text');
298
 
299
  if (type === 'audio') {
300
  input.accept = "audio/*";
301
- icon.className = "fas fa-microphone-alt text-3xl text-teal-500 mb-2";
302
- text.innerText = "Tap to upload Cough Audio (.wav/.mp3)";
303
  } else {
304
  input.accept = "image/*";
305
- icon.className = "fas fa-cloud-upload-alt text-3xl text-gray-400 mb-2";
306
- text.innerText = "Tap to upload Image";
307
  }
 
308
 
309
- // Reset
310
  document.getElementById('inputs').classList.remove('opacity-50', 'pointer-events-none');
311
  document.getElementById('result-box').classList.add('hidden');
312
  document.getElementById('run-btn').classList.add('hidden');
@@ -361,7 +474,7 @@ def home():
361
  document.getElementById('res-label').innerText = "Rejected";
362
  document.getElementById('res-conf').innerText = "--";
363
  updateBadge("INVALID", "bg-gray-200", "text-gray-700");
364
- showAlert(data.error || "Invalid File", "bg-gray-100");
365
  return;
366
  }
367
 
@@ -372,10 +485,10 @@ def home():
372
 
373
  if (data.risk === "HIGH") {
374
  updateBadge("HIGH RISK", "bg-red-100", "text-red-700");
375
- showAlert("Respiratory Symptom Detected. Refer to PHC.", "bg-red-50", "text-red-800");
376
  } else if (data.risk === "MODERATE") {
377
  updateBadge("MODERATE", "bg-yellow-100", "text-yellow-700");
378
- showAlert("Moderate Risk. Monitor symptoms.", "bg-yellow-50", "text-yellow-800");
379
  } else {
380
  updateBadge("LOW RISK", "bg-green-100", "text-green-700");
381
  document.getElementById('alert-box').classList.add('hidden');
@@ -401,7 +514,7 @@ def home():
401
  }
402
  function showAlert(msg, bg, color) {
403
  let a = document.getElementById('alert-box');
404
- a.className = `mt-4 p-3 rounded border border-gray-200 text-sm ${bg} ${color || ''}`;
405
  a.classList.remove('hidden');
406
  document.getElementById('alert-text').innerText = msg;
407
  }
 
20
  "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
21
  "desc": "Tuberculosis & Pneumonia (Chest X-Ray)",
22
  "safe": ["NORMAL", "normal", "No Pneumonia"],
23
+ "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W X-Ray."}
24
  },
25
  "cough": {
26
  "type": "audio",
27
  "id": "MIT/ast-finetuned-audioset-10-10-0.4593",
28
  "desc": "COPD & Respiratory Screening (Cough Audio)",
29
  "target_labels": ["Cough", "Throat clearing", "Respiratory sounds", "Wheeze"],
30
+ "rules": {"min_duration": 0.5, "reject_msg": "Invalid: Audio too short or silent."}
31
+ },
32
+ "fracture": {
33
+ "type": "image",
34
+ "id": "dima806/bone_fracture_detection",
35
+ "desc": "Bone Fracture Detection (X-Ray)",
36
+ "safe": ["normal", "healed"],
37
+ "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W X-Ray."}
38
+ },
39
+ "brain": {
40
+ "type": "image",
41
+ "id": "Hemgg/brain-tumor-classification",
42
+ "desc": "Brain Tumor & Stroke Screening (MRI/CT)",
43
+ "safe": ["no_tumor"],
44
+ "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W MRI Scan."}
45
  },
46
  "eye": {
47
  "type": "image",
48
  "id": "AventIQ-AI/resnet18-cataract-detection-system",
49
  "desc": "Cataract Detection (Eye Photo)",
50
  "safe": ["Normal", "normal", "healthy"],
51
+ "rules": {"min_sat": 20, "min_white": 0.05, "reject_msg": "Invalid: No eye detected."}
52
  },
53
  "skin": {
54
  "type": "image",
55
  "id": "Anwarkh1/Skin_Cancer-Image_Classification",
56
  "desc": "Dermatology Analysis (Lesion Photo)",
57
  "safe": ["Benign", "benign", "nv", "bkl"],
58
+ "rules": {"min_sat": 20, "max_white": 0.25, "reject_msg": "Invalid: Not a skin close-up."}
59
  }
60
  }
61
 
 
73
  transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
74
  ])
75
 
 
76
  def validate_image(self, image, task):
77
  rules = MODELS[task].get("rules", {})
78
  img_hsv = image.convert('HSV')
 
84
  white_pixels = np.logical_and(img_np[:,:,1] < 40, img_np[:,:,2] > 180)
85
  white_ratio = np.sum(white_pixels) / white_pixels.size
86
 
87
+ if "max_sat" in rules and avg_sat > rules["max_sat"]: return False, rules["reject_msg"]
88
+ if "min_sat" in rules and avg_sat < rules["min_sat"]: return False, rules["reject_msg"]
89
+ if "min_white" in rules and white_ratio < rules["min_white"]: return False, rules["reject_msg"]
90
+ if "max_white" in rules and white_ratio > rules["max_white"]: return False, rules["reject_msg"]
91
 
92
  return True, ""
93
 
 
94
  def validate_audio(self, audio_array, sr):
95
  duration = len(audio_array) / sr
96
  if duration < 0.5: return False, "Audio too short (< 0.5s)."
97
  if np.max(np.abs(audio_array)) < 0.01: return False, "Audio is silent/empty."
98
  return True, ""
99
 
 
100
  def predict(self, file_bytes, task):
101
  model_cfg = MODELS[task]
102
 
 
103
  if model_cfg["type"] == "audio":
 
104
  try:
 
105
  audio, sr = librosa.load(io.BytesIO(file_bytes), sr=16000)
 
 
106
  is_valid, msg = self.validate_audio(audio, sr)
107
  if not is_valid: return {"error": msg, "risk": "INVALID"}
108
 
 
109
  classifier = pipeline("audio-classification", model=model_cfg["id"])
 
110
  sf.write("temp.wav", audio, sr)
111
  outputs = classifier("temp.wav")
112
 
 
113
  top = outputs[0]
114
  target_labels = model_cfg["target_labels"]
 
 
115
  is_cough = any(target in res['label'] for res in outputs[:3] for target in target_labels)
116
 
117
  risk = "HIGH" if is_cough and top['score'] > 0.4 else "LOW"
118
  label = f"Detected: {top['label']}" if is_cough else "Normal / Background Noise"
119
 
120
+ return {"task": task, "desc": model_cfg["desc"], "prediction": {"label": label, "score": top['score']}, "risk": risk}
 
 
 
 
 
121
  except Exception as e:
122
  return {"error": f"Audio Error: {str(e)}"}
123
 
 
124
  else:
 
125
  try:
126
  image = Image.open(io.BytesIO(file_bytes)).convert("RGB")
127
  is_valid, msg = self.validate_image(image, task)
 
132
  model.to(self.device)
133
  model.eval()
134
 
135
+ inputs = self.img_transform(image).unsqueeze(0).to(self.device)
136
  with torch.no_grad():
137
  outputs = model(inputs)
138
  probs = F.softmax(outputs.logits, dim=-1)
 
146
 
147
  if top["score"] < 0.5: risk = "UNCERTAIN"
148
  elif is_safe: risk = "LOW"
149
+ else: risk = "HIGH" if top["score"] > 0.70 else "MODERATE"
150
 
151
  del model
152
  gc.collect()
 
174
  <!DOCTYPE html>
175
  <html>
176
  <head>
177
+ <title>MediScan Rural | Govt of Meghalaya</title>
178
  <script src="https://cdn.tailwindcss.com"></script>
179
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
180
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
181
  </head>
182
  <body class="bg-slate-50 min-h-screen flex flex-col font-sans">
183
 
184
+ <nav class="bg-white border-b-4 border-blue-900 shadow-sm p-3">
185
  <div class="container mx-auto flex justify-between items-center">
186
+ <div class="flex items-center gap-4">
187
+ <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Seal_of_Meghalaya.svg/150px-Seal_of_Meghalaya.svg.png" class="h-16">
188
  <div>
189
+ <h2 class="text-sm font-bold text-gray-500 uppercase tracking-widest" data-translate="govt_title">GOVERNMENT OF MEGHALAYA</h2>
190
+ <h1 class="text-2xl font-extrabold text-blue-900 tracking-tight">MediScan Rural AI</h1>
191
+ <p class="text-xs text-green-600 font-bold flex items-center gap-1">
192
+ <span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
193
+ <span data-translate="online_status">Online Node: Shillong HQ</span>
194
+ </p>
195
  </div>
196
  </div>
197
+
198
+ <div class="flex flex-col items-end gap-2">
199
+ <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 focus:border-blue-500 block p-2">
200
+ <option value="en">🇬🇧 English</option>
201
+ <option value="hi">🇮🇳 हिंदी (Hindi)</option>
202
+ <option value="as">🇮🇳 অসমীয়া (Assamese)</option>
203
+ <option value="kha">🌲 Khasi</option>
204
+ <option value="gar">⛰️ Garo</option>
205
+ </select>
206
+
207
+ <div class="hidden md:flex items-center gap-2 bg-blue-50 px-3 py-1 rounded text-xs font-bold border border-blue-100 text-blue-800">
208
+ <img src="https://upload.wikimedia.org/wikipedia/en/c/cf/Aadhaar_Logo.svg" class="h-4">
209
+ <span data-translate="abha_link">ABHA Linked</span>
210
+ </div>
211
  </div>
212
  </div>
213
  </nav>
214
 
215
+ <div class="container mx-auto mt-6 p-4 max-w-5xl flex-grow">
216
 
217
+ <div class="grid grid-cols-3 md:grid-cols-6 gap-3 mb-6">
218
+ <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">
219
+ <i class="fas fa-lungs text-blue-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
220
+ <span data-translate="btn_lungs">Lungs</span>
221
  </button>
222
+ <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">
223
+ <i class="fas fa-head-side-cough text-teal-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
224
+ <span data-translate="btn_cough">Cough/COPD</span>
225
  </button>
226
+ <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">
227
+ <i class="fas fa-bone text-slate-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
228
+ <span data-translate="btn_bone">Fracture</span>
229
  </button>
230
+ <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">
231
+ <i class="fas fa-brain text-purple-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
232
+ <span data-translate="btn_brain">Brain</span>
233
+ </button>
234
+ <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">
235
+ <i class="fas fa-eye text-indigo-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
236
+ <span data-translate="btn_eye">Eye</span>
237
+ </button>
238
+ <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">
239
+ <i class="fas fa-hand-dots text-orange-500 text-3xl mb-2 group-hover:scale-110 transition-transform"></i>
240
+ <span data-translate="btn_skin">Skin</span>
241
  </button>
242
  </div>
243
 
244
+ <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-100">
245
+ <h2 id="header-text" class="text-xl font-bold text-gray-700 mb-6 text-center border-b pb-4">Select a Category</h2>
246
 
247
  <div id="inputs" class="opacity-50 pointer-events-none transition-opacity mb-6">
248
  <div class="grid grid-cols-2 gap-4 mb-4">
249
+ <div>
250
+ <label class="text-xs font-bold text-gray-400 uppercase" data-translate="lbl_name">Patient Name</label>
251
+ <input type="text" id="p-name" class="w-full border p-2 rounded bg-gray-50 outline-none focus:border-blue-500">
252
+ </div>
253
+ <div>
254
+ <label class="text-xs font-bold text-gray-400 uppercase" data-translate="lbl_age">Age / ID</label>
255
+ <input type="text" class="w-full border p-2 rounded bg-gray-50 outline-none focus:border-blue-500">
256
+ </div>
257
  </div>
258
 
259
+ <div onclick="document.getElementById('file-input').click()" class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:bg-blue-50 transition group">
260
  <input type="file" id="file-input" class="hidden" onchange="showPreview(event)" onclick="this.value=null">
261
 
262
+ <div id="placeholder" class="group-hover:scale-105 transition-transform">
263
+ <i id="upload-icon" class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
264
+ <p id="upload-text" class="text-gray-500 font-medium" data-translate="txt_upload">Tap to upload</p>
265
  </div>
266
 
267
+ <img id="img-preview" class="hidden mx-auto max-h-56 rounded shadow object-contain">
268
  <audio id="audio-preview" controls class="hidden w-full mt-4"></audio>
269
  </div>
270
  </div>
271
 
272
+ <button id="run-btn" onclick="analyze()" class="hidden w-full bg-blue-900 hover:bg-blue-800 text-white font-bold py-4 rounded-xl shadow-lg transition transform hover:scale-[1.02] flex items-center justify-center gap-2">
273
+ <i class="fas fa-microscope"></i> <span data-translate="btn_run">Run Diagnosis</span>
274
  </button>
275
 
276
  <div id="loader" class="hidden text-center py-6">
277
  <div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"></div>
278
+ <p class="text-sm text-gray-500 mt-2 font-semibold" data-translate="txt_analyzing">Analyzing Data...</p>
279
  </div>
280
 
281
+ <div id="result-box" class="hidden mt-6 border-t pt-6 bg-gray-50 -mx-6 -mb-6 p-6 rounded-b-2xl">
282
  <div class="flex justify-between items-start">
283
  <div>
284
+ <p class="text-xs font-bold text-gray-400 uppercase" data-translate="lbl_result">Analysis Result</p>
285
  <h1 id="res-label" class="text-2xl font-extrabold text-gray-800">--</h1>
286
+ <p class="text-sm text-gray-600 mt-1"><span data-translate="lbl_conf">Confidence</span>: <span id="res-conf" class="font-mono font-bold">--</span></p>
287
  </div>
288
  <span id="res-badge" class="px-3 py-1 rounded text-sm font-bold uppercase">--</span>
289
  </div>
290
 
291
+ <div id="alert-box" class="hidden mt-4 p-4 rounded-lg border text-sm flex items-start gap-3 shadow-sm">
292
+ <i class="fas fa-info-circle text-xl mt-0.5"></i>
293
+ <div>
294
+ <strong class="block" data-translate="lbl_action">Action Required</strong>
295
+ <span id="alert-text">--</span>
296
+ </div>
297
  </div>
298
 
299
+ <div class="mt-4 flex items-center justify-between text-xs text-gray-500 border-t pt-2">
300
+ <span class="font-bold flex items-center gap-1"><i class="fas fa-database"></i> <span data-translate="lbl_sync">Govt Sync</span></span>
301
  <span id="sync-msg" class="text-yellow-600"><i class="fas fa-sync fa-spin"></i> Pending...</span>
302
  </div>
303
  </div>
 
306
  </div>
307
 
308
  <script>
309
+ // TRANSLATION DICTIONARY
310
+ const TRANSLATIONS = {
311
+ en: {
312
+ govt_title: "GOVERNMENT OF MEGHALAYA",
313
+ online_status: "Online Node: Shillong HQ",
314
+ abha_link: "ABHA Linked",
315
+ btn_lungs: "Lungs", btn_cough: "Cough/COPD", btn_bone: "Fracture", btn_brain: "Brain", btn_eye: "Eye", btn_skin: "Skin",
316
+ lbl_name: "Patient Name", lbl_age: "Age / ID",
317
+ txt_upload: "Tap to upload Scan/Photo",
318
+ btn_run: "Run Diagnosis",
319
+ txt_analyzing: "Downloading AI Model & Analyzing...",
320
+ lbl_result: "Analysis Result", lbl_conf: "Confidence",
321
+ lbl_action: "Action Required", lbl_sync: "Govt Sync"
322
+ },
323
+ hi: {
324
+ govt_title: "मेघालय सरकार",
325
+ online_status: "ऑनलाइन नोड: शिलांग मुख्यालय",
326
+ abha_link: "ABHA लिंक किया गया",
327
+ btn_lungs: "फेफड़े", btn_cough: "खांसी/COPD", btn_bone: "हड्डी", btn_brain: "मस्तिष्क", btn_eye: "आंख", btn_skin: "त्वचा",
328
+ lbl_name: "रोगी का नाम", lbl_age: "आयु / आईडी",
329
+ txt_upload: "फोटो अपलोड करें",
330
+ btn_run: "जांच शुरू करें",
331
+ txt_analyzing: "विश्लेषण कर रहा है...",
332
+ lbl_result: "परिणाम", lbl_conf: "विश्वास स्तर",
333
+ lbl_action: "आवश्यक कार्रवाई", lbl_sync: "सरकारी सिंक"
334
+ },
335
+ as: {
336
+ govt_title: "মেঘালয় চৰকাৰ",
337
+ online_status: "অনলাইন ন'ড: শ্বিলং",
338
+ abha_link: "ABHA সংযুক্ত",
339
+ btn_lungs: "হাঁওফাঁও", btn_cough: "কাহ", btn_bone: "হাৰ ভঙা", btn_brain: "মগজু", btn_eye: "চকু", btn_skin: "ছাল",
340
+ lbl_name: "ৰোগীৰ নাম", lbl_age: "বয়স / আইডি",
341
+ txt_upload: "ফটো আপলোড কৰক",
342
+ btn_run: "পৰীক্ষা কৰক",
343
+ txt_analyzing: "বিশ্লেষণ চলি আছে...",
344
+ lbl_result: "ফলাফল", lbl_conf: "নিশ্চয়তা",
345
+ lbl_action: "পৰৱৰ্তী পদক্ষেপ", lbl_sync: "চৰকাৰী ডাটাবেচ"
346
+ },
347
+ kha: { // Khasi Language
348
+ govt_title: "SORKAR MEGHALAYA",
349
+ online_status: "Online: Shillong",
350
+ abha_link: "ABHA Link",
351
+ btn_lungs: "Phopsa", btn_cough: "Jyrhoh", btn_bone: "Shyieng", btn_brain: "Jabieng", btn_eye: "Khmat", btn_skin: "Doh",
352
+ lbl_name: "Kyrteng Nongpang", lbl_age: "Rta / ID",
353
+ txt_upload: "Thep ia ka dur",
354
+ btn_run: "Leh Test",
355
+ txt_analyzing: "Dang Check...",
356
+ lbl_result: "Result", lbl_conf: "Jingshisha",
357
+ lbl_action: "Leh ia kane", lbl_sync: "Sorkar Sync"
358
+ },
359
+ gar: { // Garo Language
360
+ govt_title: "MEGHALAYA SORKARI",
361
+ online_status: "Online: Shillong",
362
+ abha_link: "ABHA Link",
363
+ btn_lungs: "Ka'sop", btn_cough: "Gusuk", btn_bone: "Greng", btn_brain: "Taning", btn_eye: "Mikron", btn_skin: "Bigil",
364
+ lbl_name: "Sagipani Bimung", lbl_age: "Bilsi / ID",
365
+ txt_upload: "Photo Gata",
366
+ btn_run: "Porikka Ka'bo",
367
+ txt_analyzing: "Niyenga...",
368
+ lbl_result: "Result", lbl_conf: "Bebera'ani",
369
+ lbl_action: "Nangchongmotgipa Kam", lbl_sync: "Sorkar Sync"
370
+ }
371
+ };
372
+
373
  let currTask = null;
374
  let currType = 'image';
375
  let currFile = null;
376
+ let currLang = 'en';
377
+
378
+ function changeLanguage() {
379
+ currLang = document.getElementById('lang-select').value;
380
+ let t = TRANSLATIONS[currLang];
381
+
382
+ document.querySelectorAll('[data-translate]').forEach(el => {
383
+ let key = el.getAttribute('data-translate');
384
+ if (t[key]) el.innerText = t[key];
385
+ });
386
+
387
+ // Refresh Header Text if task selected
388
+ if (currTask) {
389
+ let taskKey = "btn_" + currTask;
390
+ document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${t[taskKey]}</span>`;
391
+ }
392
+ }
393
 
394
  function setTask(task, type) {
395
  currTask = task;
396
  currType = type;
397
+ let t = TRANSLATIONS[currLang];
398
 
399
+ // Buttons
400
+ document.querySelectorAll('button[id^="btn-"]').forEach(b => b.classList.remove('ring-2', 'ring-blue-400', 'border-blue-500', 'bg-blue-50'));
401
+ let btn = document.getElementById('btn-'+task);
402
+ btn.classList.add('ring-2', 'ring-blue-400', 'border-blue-500', 'bg-blue-50');
403
 
404
+ // Text
405
+ let taskName = t["btn_" + task];
406
  document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${taskName}</span>`;
407
 
408
+ // Input Logic
409
  let input = document.getElementById('file-input');
410
  let icon = document.getElementById('upload-icon');
411
  let text = document.getElementById('upload-text');
412
 
413
  if (type === 'audio') {
414
  input.accept = "audio/*";
415
+ icon.className = "fas fa-microphone-alt text-4xl text-teal-500 mb-2";
 
416
  } else {
417
  input.accept = "image/*";
418
+ icon.className = "fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2";
 
419
  }
420
+ text.innerText = t['txt_upload'];
421
 
422
+ // Reset UI
423
  document.getElementById('inputs').classList.remove('opacity-50', 'pointer-events-none');
424
  document.getElementById('result-box').classList.add('hidden');
425
  document.getElementById('run-btn').classList.add('hidden');
 
474
  document.getElementById('res-label').innerText = "Rejected";
475
  document.getElementById('res-conf').innerText = "--";
476
  updateBadge("INVALID", "bg-gray-200", "text-gray-700");
477
+ showAlert(data.error, "bg-gray-100", "text-gray-800");
478
  return;
479
  }
480
 
 
485
 
486
  if (data.risk === "HIGH") {
487
  updateBadge("HIGH RISK", "bg-red-100", "text-red-700");
488
+ showAlert("Critical Issue. Referral Needed.", "bg-red-50", "text-red-800");
489
  } else if (data.risk === "MODERATE") {
490
  updateBadge("MODERATE", "bg-yellow-100", "text-yellow-700");
491
+ showAlert("Moderate Risk. Monitor.", "bg-yellow-50", "text-yellow-800");
492
  } else {
493
  updateBadge("LOW RISK", "bg-green-100", "text-green-700");
494
  document.getElementById('alert-box').classList.add('hidden');
 
514
  }
515
  function showAlert(msg, bg, color) {
516
  let a = document.getElementById('alert-box');
517
+ a.className = `mt-4 p-4 rounded-lg border flex items-start gap-3 ${bg} ${color}`;
518
  a.classList.remove('hidden');
519
  document.getElementById('alert-text').innerText = msg;
520
  }