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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +201 -169
app.py CHANGED
@@ -1,161 +1,169 @@
1
  import torch
2
  import torch.nn.functional as F
3
- from transformers import AutoModelForImageClassification
4
  from torchvision import transforms
5
  from PIL import Image, ImageStat
6
  from fastapi import FastAPI, File, UploadFile
7
  from fastapi.responses import HTMLResponse
 
8
  import io
9
  import gc
 
 
10
 
11
  # ==========================================
12
- # 1. CONFIGURATION (With STRICT Guardrails)
13
  # ==========================================
14
  MODELS = {
15
  "lungs": {
 
16
  "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
17
  "desc": "Tuberculosis & Pneumonia (Chest X-Ray)",
18
  "safe": ["NORMAL", "normal", "No Pneumonia"],
19
- # Rule: Saturation must be LOW (Grayscale)
20
- "guardrails": {"max_sat": 35}
21
  },
22
- "blood": {
23
- "id": "mrm8488/vit-base-patch16-224-finetuned-malaria-detection",
24
- "desc": "Malaria Screening (Microscopic Slide)",
25
- "safe": ["Uninfected", "uninfected"],
26
- # Rule: Must be bright (Backlit slide)
27
- "guardrails": {"min_bright": 60}
28
  },
29
  "eye": {
 
30
  "id": "AventIQ-AI/resnet18-cataract-detection-system",
31
- "desc": "Cataract Detection (Smartphone Eye Photo)",
32
  "safe": ["Normal", "normal", "healthy"],
33
- # Rule: Saturation must be HIGH (Color photo) - Blocks X-Rays
34
- "guardrails": {"min_sat": 20}
35
  },
36
  "skin": {
 
37
  "id": "Anwarkh1/Skin_Cancer-Image_Classification",
38
- "desc": "Dermatology & Lesion Analysis",
39
  "safe": ["Benign", "benign", "nv", "bkl"],
40
- # Rule: Saturation must be HIGH (Color photo) - Blocks X-Rays
41
- "guardrails": {"min_sat": 20}
42
  }
43
  }
44
 
45
  # ==========================================
46
- # 2. AI ENGINE
47
  # ==========================================
48
  class MedicalEngine:
49
  def __init__(self):
50
  self.device = "cpu"
51
  print("✅ System Initialized: Medical Engine Ready")
52
 
53
- self.transform = transforms.Compose([
54
  transforms.Resize((224, 224)),
55
  transforms.ToTensor(),
56
  transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
57
  ])
58
 
 
59
  def validate_image(self, image, task):
60
- """
61
- Universal Guardrails:
62
- - Prevents X-Rays in Skin/Eye tabs (Checks Min Saturation)
63
- - Prevents Selfies in X-Ray tab (Checks Max Saturation)
64
- """
65
- rules = MODELS[task].get("guardrails", {})
66
-
67
- # Convert to HSV (Hue, Saturation, Value)
68
- # Saturation (Index 1): 0 = Gray, 255 = Color
69
- # Value (Index 2): 0 = Dark, 255 = Bright
70
- stat = ImageStat.Stat(image.convert('HSV'))
71
  avg_sat = stat.mean[1]
72
- avg_bright = stat.mean[2]
73
-
74
- # 1. Check Max Saturation (Block Colorful images)
75
- if "max_sat" in rules and avg_sat > rules["max_sat"]:
76
- return False, f"⚠️ Invalid Image: Too colorful ({int(avg_sat)}). This looks like a photo, not an X-Ray."
77
-
78
- # 2. Check Min Saturation (Block Grayscale images)
79
- if "min_sat" in rules and avg_sat < rules["min_sat"]:
80
- return False, f"⚠️ Invalid Image: Too gray ({int(avg_sat)}). This looks like an X-Ray/Doc. Please upload a color photo."
81
-
82
- # 3. Check Min Brightness (Block Dark images)
83
- if "min_bright" in rules and avg_bright < rules["min_bright"]:
84
- return False, "⚠️ Invalid Image: Too dark. Microscope slides must be backlit."
85
-
86
  return True, ""
87
 
88
- def predict(self, image_bytes, task):
89
- # A. Load Image
90
- try:
91
- image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
92
- except:
93
- return {"error": "File is not a valid image."}
94
-
95
- # B. Run Validation
96
- is_valid, msg = self.validate_image(image, task)
97
- if not is_valid:
98
- # Return a special "INVALID" state
99
- return {
100
- "task": task,
101
- "desc": MODELS[task]["desc"],
102
- "prediction": {"label": "Invalid Image", "score": 0.0},
103
- "risk": "INVALID",
104
- "error": msg
105
- }
106
 
107
- # C. Load Model
108
- print(f"⏳ Loading Model for: {task}...")
109
- try:
110
- model_id = MODELS[task]["id"]
111
- model = AutoModelForImageClassification.from_pretrained(model_id)
112
- model.to(self.device)
113
- model.eval()
114
- except Exception as e:
115
- return {"error": "Failed to load AI model. Try again."}
116
-
117
- # D. Inference
118
- try:
119
- inputs = self.transform(image).unsqueeze(0).to(self.device)
120
- with torch.no_grad():
121
- outputs = model(inputs)
122
- probs = F.softmax(outputs.logits, dim=-1)
123
-
124
- results = []
125
- for i, score in enumerate(probs[0]):
126
- label = model.config.id2label[i]
127
- results.append({"label": label, "score": float(score)})
128
- results.sort(key=lambda x: x['score'], reverse=True)
129
- top = results[0]
130
-
131
- # E. Risk Logic
132
- safe_words = MODELS[task]["safe"]
133
- is_safe = any(s.lower() in top["label"].lower() for s in safe_words)
134
-
135
- if top["score"] < 0.5:
136
- risk = "UNCERTAIN"
137
- top["label"] = "Inconclusive / Unknown"
138
- elif is_safe:
139
- risk = "LOW"
140
- else:
141
- risk = "HIGH" if top["score"] > 0.75 else "MODERATE"
142
-
143
- except Exception as e:
144
- return {"error": f"Prediction Error: {str(e)}"}
145
 
146
- finally:
147
- del model
148
- gc.collect()
149
-
150
- return {
151
- "task": task,
152
- "desc": MODELS[task]["desc"],
153
- "prediction": top,
154
- "risk": risk
155
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
  # ==========================================
158
- # 3. API & UI
159
  # ==========================================
160
  app = FastAPI()
161
  engine = MedicalEngine()
@@ -198,16 +206,16 @@ def home():
198
  <div class="container mx-auto mt-8 p-4 max-w-3xl flex-grow">
199
 
200
  <div class="grid grid-cols-4 gap-2 mb-6">
201
- <button onclick="setTask('lungs')" 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">
202
  <i class="fas fa-lungs text-blue-500 block text-2xl mb-1"></i> Lungs
203
  </button>
204
- <button onclick="setTask('blood')" id="btn-blood" class="p-3 bg-white rounded-lg shadow hover:bg-red-50 border-2 border-transparent transition text-sm font-bold text-gray-600">
205
- <i class="fas fa-burn text-red-500 block text-2xl mb-1"></i> Malaria
206
  </button>
207
- <button onclick="setTask('eye')" 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">
208
  <i class="fas fa-eye text-indigo-500 block text-2xl mb-1"></i> Eye
209
  </button>
210
- <button onclick="setTask('skin')" 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">
211
  <i class="fas fa-hand-dots text-orange-500 block text-2xl mb-1"></i> Skin
212
  </button>
213
  </div>
@@ -222,12 +230,15 @@ def home():
222
  </div>
223
 
224
  <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">
225
- <input type="file" id="file-input" class="hidden" accept="image/*" onchange="showPreview(event)" onclick="this.value=null">
 
226
  <div id="placeholder">
227
- <i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-2"></i>
228
- <p class="text-gray-500 text-sm">Tap to upload image</p>
229
  </div>
230
- <img id="preview" class="hidden mx-auto max-h-48 rounded shadow object-contain">
 
 
231
  </div>
232
  </div>
233
 
@@ -237,14 +248,14 @@ def home():
237
 
238
  <div id="loader" class="hidden text-center py-6">
239
  <div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"></div>
240
- <p class="text-sm text-gray-500 mt-2 font-semibold">Downloading AI Model & Analyzing...</p>
241
  </div>
242
 
243
  <div id="result-box" class="hidden mt-6 border-t pt-6">
244
  <div class="flex justify-between items-start">
245
  <div>
246
- <p class="text-xs font-bold text-gray-400 uppercase">Prediction</p>
247
- <h1 id="res-label" class="text-3xl font-extrabold text-gray-800">--</h1>
248
  <p class="text-sm text-gray-600 mt-1">Confidence: <span id="res-conf" class="font-mono font-bold">--</span></p>
249
  </div>
250
  <span id="res-badge" class="px-3 py-1 rounded text-sm font-bold uppercase">--</span>
@@ -265,34 +276,66 @@ def home():
265
 
266
  <script>
267
  let currTask = null;
 
268
  let currFile = null;
269
 
270
- function setTask(task) {
271
  currTask = task;
 
 
 
272
  document.querySelectorAll('button[id^="btn-"]').forEach(b => b.classList.remove('ring-2', 'ring-blue-400', 'border-blue-500'));
273
  document.getElementById('btn-'+task).classList.add('ring-2', 'ring-blue-400', 'border-blue-500');
274
- document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${task}</span> Image`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  document.getElementById('inputs').classList.remove('opacity-50', 'pointer-events-none');
276
  document.getElementById('result-box').classList.add('hidden');
277
  document.getElementById('run-btn').classList.add('hidden');
278
  document.getElementById('placeholder').classList.remove('hidden');
279
- document.getElementById('preview').classList.add('hidden');
280
- document.getElementById('preview').src = "";
281
  currFile = null;
282
  }
283
 
284
  function showPreview(event) {
285
  if (event.target.files && event.target.files[0]) {
286
  currFile = event.target.files[0];
287
- let reader = new FileReader();
288
- reader.onload = function(e) {
289
- document.getElementById('preview').src = e.target.result;
290
- document.getElementById('preview').classList.remove('hidden');
291
- document.getElementById('placeholder').classList.add('hidden');
292
- document.getElementById('run-btn').classList.remove('hidden');
293
- document.getElementById('result-box').classList.add('hidden');
294
- };
295
- reader.readAsDataURL(currFile);
 
 
 
 
 
 
 
 
296
  }
297
  }
298
 
@@ -315,47 +358,28 @@ def home():
315
  document.getElementById('result-box').classList.remove('hidden');
316
 
317
  if (data.risk === "INVALID") {
318
- document.getElementById('res-label').innerText = "Image Rejected";
319
  document.getElementById('res-conf').innerText = "--";
320
- let badge = document.getElementById('res-badge');
321
- badge.className = "px-3 py-1 rounded text-sm font-bold uppercase bg-gray-200 text-gray-700";
322
- badge.innerText = "INVALID";
323
-
324
- let alertBox = document.getElementById('alert-box');
325
- alertBox.className = "mt-4 p-3 bg-gray-100 text-gray-800 rounded border border-gray-300 text-sm";
326
- alertBox.classList.remove('hidden');
327
- document.getElementById('alert-text').innerText = data.error;
328
  return;
329
  }
330
 
331
- if (data.error) { alert("Error: " + data.error); resetLoading(); return; }
332
 
333
  document.getElementById('res-label').innerText = data.prediction.label;
334
  document.getElementById('res-conf').innerText = (data.prediction.score * 100).toFixed(1) + "%";
335
 
336
- let badge = document.getElementById('res-badge');
337
- let alertBox = document.getElementById('alert-box');
338
-
339
  if (data.risk === "HIGH") {
340
- badge.className = "px-3 py-1 rounded text-sm font-bold uppercase bg-red-100 text-red-700";
341
- alertBox.className = "mt-4 p-3 bg-red-50 text-red-800 rounded border border-red-200 text-sm";
342
- alertBox.classList.remove('hidden');
343
- document.getElementById('alert-text').innerText = "High Risk. Immediate Referral Recommended.";
344
  } else if (data.risk === "MODERATE") {
345
- badge.className = "px-3 py-1 rounded text-sm font-bold uppercase bg-yellow-100 text-yellow-700";
346
- alertBox.className = "mt-4 p-3 bg-yellow-50 text-yellow-800 rounded border border-yellow-200 text-sm";
347
- alertBox.classList.remove('hidden');
348
- document.getElementById('alert-text').innerText = "Moderate Risk. Consult Doctor.";
349
- } else if (data.risk === "UNCERTAIN") {
350
- badge.className = "px-3 py-1 rounded text-sm font-bold uppercase bg-gray-200 text-gray-700";
351
- alertBox.className = "mt-4 p-3 bg-gray-100 text-gray-800 rounded border border-gray-200 text-sm";
352
- alertBox.classList.remove('hidden');
353
- document.getElementById('alert-text').innerText = "Image Unclear. Retake Photo.";
354
  } else {
355
- badge.className = "px-3 py-1 rounded text-sm font-bold uppercase bg-green-100 text-green-700";
356
- alertBox.classList.add('hidden');
357
  }
358
- badge.innerText = data.risk + " RISK";
359
 
360
  setTimeout(() => {
361
  document.getElementById('sync-msg').innerHTML = "<i class='fas fa-check-circle'></i> Synced!";
@@ -363,15 +387,23 @@ def home():
363
  }, 2000);
364
 
365
  } catch (e) {
366
- alert("Connection Failed. Refresh and try again.");
367
  console.error(e);
368
- resetLoading();
 
369
  }
370
  }
371
 
372
- function resetLoading() {
373
- document.getElementById('loader').classList.add('hidden');
374
- document.getElementById('run-btn').classList.remove('hidden');
 
 
 
 
 
 
 
375
  }
376
  </script>
377
  </body>
 
1
  import torch
2
  import torch.nn.functional as F
3
+ from transformers import AutoModelForImageClassification, pipeline
4
  from torchvision import transforms
5
  from PIL import Image, ImageStat
6
  from fastapi import FastAPI, File, UploadFile
7
  from fastapi.responses import HTMLResponse
8
+ import numpy as np
9
  import io
10
  import gc
11
+ import librosa
12
+ import soundfile as sf
13
 
14
  # ==========================================
15
+ # 1. CONFIGURATION
16
  # ==========================================
17
  MODELS = {
18
  "lungs": {
19
+ "type": "image",
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
 
48
  # ==========================================
49
+ # 2. MEDICAL ENGINE
50
  # ==========================================
51
  class MedicalEngine:
52
  def __init__(self):
53
  self.device = "cpu"
54
  print("✅ System Initialized: Medical Engine Ready")
55
 
56
+ self.img_transform = transforms.Compose([
57
  transforms.Resize((224, 224)),
58
  transforms.ToTensor(),
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')
66
+ stat = ImageStat.Stat(img_hsv)
 
 
 
 
 
 
 
 
67
  avg_sat = stat.mean[1]
68
+
69
+ # Forensics
70
+ img_np = np.array(img_hsv)
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)
134
+ if not is_valid:
135
+ return {"task": task, "risk": "INVALID", "error": msg, "prediction": {"label": "Rejected", "score": 0.0}}
136
+
137
+ model = AutoModelForImageClassification.from_pretrained(model_cfg["id"])
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)
145
+
146
+ results = [{"label": model.config.id2label[i], "score": float(score)} for i, score in enumerate(probs[0])]
147
+ results.sort(key=lambda x: x['score'], reverse=True)
148
+ top = results[0]
149
+
150
+ safe_words = model_cfg["safe"]
151
+ is_safe = any(s.lower() in top["label"].lower() for s in safe_words)
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()
159
+
160
+ return {"task": task, "desc": model_cfg["desc"], "prediction": top, "risk": risk}
161
+
162
+ except Exception as e:
163
+ return {"error": f"Image Error: {str(e)}"}
164
 
165
  # ==========================================
166
+ # 3. API & FRONTEND
167
  # ==========================================
168
  app = FastAPI()
169
  engine = MedicalEngine()
 
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>
 
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
 
 
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>
 
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');
313
  document.getElementById('placeholder').classList.remove('hidden');
314
+ document.getElementById('img-preview').classList.add('hidden');
315
+ document.getElementById('audio-preview').classList.add('hidden');
316
  currFile = null;
317
  }
318
 
319
  function showPreview(event) {
320
  if (event.target.files && event.target.files[0]) {
321
  currFile = event.target.files[0];
322
+ let url = URL.createObjectURL(currFile);
323
+
324
+ if (currType === 'audio') {
325
+ let aud = document.getElementById('audio-preview');
326
+ aud.src = url;
327
+ aud.classList.remove('hidden');
328
+ document.getElementById('img-preview').classList.add('hidden');
329
+ } else {
330
+ let img = document.getElementById('img-preview');
331
+ img.src = url;
332
+ img.classList.remove('hidden');
333
+ document.getElementById('audio-preview').classList.add('hidden');
334
+ }
335
+
336
+ document.getElementById('placeholder').classList.add('hidden');
337
+ document.getElementById('run-btn').classList.remove('hidden');
338
+ document.getElementById('result-box').classList.add('hidden');
339
  }
340
  }
341
 
 
358
  document.getElementById('result-box').classList.remove('hidden');
359
 
360
  if (data.risk === "INVALID") {
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
 
368
+ if (data.error) { alert("Error: " + data.error); document.getElementById('run-btn').classList.remove('hidden'); return; }
369
 
370
  document.getElementById('res-label').innerText = data.prediction.label;
371
  document.getElementById('res-conf').innerText = (data.prediction.score * 100).toFixed(1) + "%";
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');
382
  }
 
383
 
384
  setTimeout(() => {
385
  document.getElementById('sync-msg').innerHTML = "<i class='fas fa-check-circle'></i> Synced!";
 
387
  }, 2000);
388
 
389
  } catch (e) {
390
+ alert("Connection Failed.");
391
  console.error(e);
392
+ document.getElementById('loader').classList.add('hidden');
393
+ document.getElementById('run-btn').classList.remove('hidden');
394
  }
395
  }
396
 
397
+ function updateBadge(text, bg, color) {
398
+ let b = document.getElementById('res-badge');
399
+ b.innerText = text;
400
+ b.className = `px-3 py-1 rounded text-sm font-bold uppercase ${bg} ${color}`;
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
  }
408
  </script>
409
  </body>