Update app.py
Browse files
app.py
CHANGED
|
@@ -10,6 +10,7 @@ import io
|
|
| 10 |
import gc
|
| 11 |
import librosa
|
| 12 |
import soundfile as sf
|
|
|
|
| 13 |
|
| 14 |
# ==========================================
|
| 15 |
# 1. CONFIGURATION
|
|
@@ -18,42 +19,42 @@ MODELS = {
|
|
| 18 |
"lungs": {
|
| 19 |
"type": "image",
|
| 20 |
"id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
|
| 21 |
-
"desc": "
|
| 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": "
|
| 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
|
| 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
|
| 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": "
|
| 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
|
| 57 |
"safe": ["Benign", "benign", "nv", "bkl"],
|
| 58 |
"rules": {"min_sat": 20, "max_white": 0.25, "reject_msg": "Invalid: Not a skin close-up."}
|
| 59 |
}
|
|
@@ -79,7 +80,6 @@ class MedicalEngine:
|
|
| 79 |
stat = ImageStat.Stat(img_hsv)
|
| 80 |
avg_sat = stat.mean[1]
|
| 81 |
|
| 82 |
-
# Forensics
|
| 83 |
img_np = np.array(img_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
|
|
@@ -162,11 +162,39 @@ class MedicalEngine:
|
|
| 162 |
app = FastAPI()
|
| 163 |
engine = MedicalEngine()
|
| 164 |
|
|
|
|
|
|
|
|
|
|
| 165 |
@app.post("/predict/{task}")
|
| 166 |
-
async def predict_route(task: str, file: UploadFile = File(...)):
|
| 167 |
if task not in MODELS: return {"error": "Invalid Task"}
|
|
|
|
| 168 |
content = await file.read()
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
@app.get("/", response_class=HTMLResponse)
|
| 172 |
def home():
|
|
@@ -194,20 +222,13 @@ def home():
|
|
| 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">🇮🇳
|
| 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>
|
|
@@ -242,8 +263,13 @@ def home():
|
|
| 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-
|
| 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>
|
|
@@ -252,18 +278,16 @@ def home():
|
|
| 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>
|
|
@@ -295,18 +319,43 @@ def home():
|
|
| 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>
|
|
|
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
</div>
|
| 306 |
</div>
|
| 307 |
|
| 308 |
<script>
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
const TRANSLATIONS = {
|
| 311 |
en: {
|
| 312 |
govt_title: "GOVERNMENT OF MEGHALAYA",
|
|
@@ -318,7 +367,7 @@ def home():
|
|
| 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",
|
| 322 |
},
|
| 323 |
hi: {
|
| 324 |
govt_title: "मेघालय सरकार",
|
|
@@ -330,21 +379,9 @@ def home():
|
|
| 330 |
btn_run: "जांच शुरू करें",
|
| 331 |
txt_analyzing: "विश्लेषण कर रहा है...",
|
| 332 |
lbl_result: "परिणाम", lbl_conf: "विश्वास स्तर",
|
| 333 |
-
lbl_action: "आवश्यक कार्रवाई",
|
| 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: {
|
| 348 |
govt_title: "SORKAR MEGHALAYA",
|
| 349 |
online_status: "Online: Shillong",
|
| 350 |
abha_link: "ABHA Link",
|
|
@@ -354,9 +391,9 @@ def home():
|
|
| 354 |
btn_run: "Leh Test",
|
| 355 |
txt_analyzing: "Dang Check...",
|
| 356 |
lbl_result: "Result", lbl_conf: "Jingshisha",
|
| 357 |
-
lbl_action: "Leh ia kane",
|
| 358 |
},
|
| 359 |
-
gar: {
|
| 360 |
govt_title: "MEGHALAYA SORKARI",
|
| 361 |
online_status: "Online: Shillong",
|
| 362 |
abha_link: "ABHA Link",
|
|
@@ -366,7 +403,7 @@ def home():
|
|
| 366 |
btn_run: "Porikka Ka'bo",
|
| 367 |
txt_analyzing: "Niyenga...",
|
| 368 |
lbl_result: "Result", lbl_conf: "Bebera'ani",
|
| 369 |
-
lbl_action: "Nangchongmotgipa Kam",
|
| 370 |
}
|
| 371 |
};
|
| 372 |
|
|
@@ -375,16 +412,16 @@ def home():
|
|
| 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>`;
|
|
@@ -396,16 +433,24 @@ def home():
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
let input = document.getElementById('file-input');
|
| 410 |
let icon = document.getElementById('upload-icon');
|
| 411 |
let text = document.getElementById('upload-text');
|
|
@@ -419,7 +464,6 @@ def home():
|
|
| 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');
|
|
@@ -433,7 +477,6 @@ def home():
|
|
| 433 |
if (event.target.files && event.target.files[0]) {
|
| 434 |
currFile = event.target.files[0];
|
| 435 |
let url = URL.createObjectURL(currFile);
|
| 436 |
-
|
| 437 |
if (currType === 'audio') {
|
| 438 |
let aud = document.getElementById('audio-preview');
|
| 439 |
aud.src = url;
|
|
@@ -445,7 +488,6 @@ def home():
|
|
| 445 |
img.classList.remove('hidden');
|
| 446 |
document.getElementById('audio-preview').classList.add('hidden');
|
| 447 |
}
|
| 448 |
-
|
| 449 |
document.getElementById('placeholder').classList.add('hidden');
|
| 450 |
document.getElementById('run-btn').classList.remove('hidden');
|
| 451 |
document.getElementById('result-box').classList.add('hidden');
|
|
@@ -454,7 +496,9 @@ def home():
|
|
| 454 |
|
| 455 |
async function analyze() {
|
| 456 |
if (!currTask || !currFile) return;
|
| 457 |
-
|
|
|
|
|
|
|
| 458 |
|
| 459 |
document.getElementById('run-btn').classList.add('hidden');
|
| 460 |
document.getElementById('loader').classList.remove('hidden');
|
|
@@ -463,12 +507,17 @@ def home():
|
|
| 463 |
let formData = new FormData();
|
| 464 |
formData.append("file", currFile);
|
| 465 |
|
|
|
|
| 466 |
try {
|
| 467 |
-
let
|
|
|
|
| 468 |
let data = await res.json();
|
| 469 |
|
| 470 |
document.getElementById('loader').classList.add('hidden');
|
| 471 |
document.getElementById('result-box').classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
| 472 |
|
| 473 |
if (data.risk === "INVALID") {
|
| 474 |
document.getElementById('res-label').innerText = "Rejected";
|
|
@@ -494,11 +543,6 @@ def home():
|
|
| 494 |
document.getElementById('alert-box').classList.add('hidden');
|
| 495 |
}
|
| 496 |
|
| 497 |
-
setTimeout(() => {
|
| 498 |
-
document.getElementById('sync-msg').innerHTML = "<i class='fas fa-check-circle'></i> Synced!";
|
| 499 |
-
document.getElementById('sync-msg').className = "text-green-600 font-bold";
|
| 500 |
-
}, 2000);
|
| 501 |
-
|
| 502 |
} catch (e) {
|
| 503 |
alert("Connection Failed.");
|
| 504 |
console.error(e);
|
|
@@ -507,6 +551,41 @@ def home():
|
|
| 507 |
}
|
| 508 |
}
|
| 509 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
function updateBadge(text, bg, color) {
|
| 511 |
let b = document.getElementById('res-badge');
|
| 512 |
b.innerText = text;
|
|
|
|
| 10 |
import gc
|
| 11 |
import librosa
|
| 12 |
import soundfile as sf
|
| 13 |
+
from datetime import datetime
|
| 14 |
|
| 15 |
# ==========================================
|
| 16 |
# 1. CONFIGURATION
|
|
|
|
| 19 |
"lungs": {
|
| 20 |
"type": "image",
|
| 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. Upload 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 |
"target_labels": ["Cough", "Throat clearing", "Respiratory sounds", "Wheeze"],
|
| 31 |
"rules": {"min_duration": 0.5, "reject_msg": "Invalid: Audio too short or silent."}
|
| 32 |
},
|
| 33 |
"fracture": {
|
| 34 |
"type": "image",
|
| 35 |
"id": "dima806/bone_fracture_detection",
|
| 36 |
+
"desc": "Bone Trauma X-Ray",
|
| 37 |
"safe": ["normal", "healed"],
|
| 38 |
"rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W X-Ray."}
|
| 39 |
},
|
| 40 |
"brain": {
|
| 41 |
"type": "image",
|
| 42 |
"id": "Hemgg/brain-tumor-classification",
|
| 43 |
+
"desc": "Brain MRI Scan Analysis",
|
| 44 |
"safe": ["no_tumor"],
|
| 45 |
"rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Upload B&W MRI Scan."}
|
| 46 |
},
|
| 47 |
"eye": {
|
| 48 |
"type": "image",
|
| 49 |
"id": "AventIQ-AI/resnet18-cataract-detection-system",
|
| 50 |
+
"desc": "Ophthalmology Scan",
|
| 51 |
"safe": ["Normal", "normal", "healthy"],
|
| 52 |
"rules": {"min_sat": 20, "min_white": 0.05, "reject_msg": "Invalid: No eye detected."}
|
| 53 |
},
|
| 54 |
"skin": {
|
| 55 |
"type": "image",
|
| 56 |
"id": "Anwarkh1/Skin_Cancer-Image_Classification",
|
| 57 |
+
"desc": "Dermatology Lesion Scan",
|
| 58 |
"safe": ["Benign", "benign", "nv", "bkl"],
|
| 59 |
"rules": {"min_sat": 20, "max_white": 0.25, "reject_msg": "Invalid: Not a skin close-up."}
|
| 60 |
}
|
|
|
|
| 80 |
stat = ImageStat.Stat(img_hsv)
|
| 81 |
avg_sat = stat.mean[1]
|
| 82 |
|
|
|
|
| 83 |
img_np = np.array(img_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
|
|
|
|
| 162 |
app = FastAPI()
|
| 163 |
engine = MedicalEngine()
|
| 164 |
|
| 165 |
+
# IN-MEMORY HISTORY STORAGE
|
| 166 |
+
HISTORY = []
|
| 167 |
+
|
| 168 |
@app.post("/predict/{task}")
|
| 169 |
+
async def predict_route(task: str, patient: str, age: str, file: UploadFile = File(...)):
|
| 170 |
if task not in MODELS: return {"error": "Invalid Task"}
|
| 171 |
+
|
| 172 |
content = await file.read()
|
| 173 |
+
result = engine.predict(content, task)
|
| 174 |
+
|
| 175 |
+
# Save to History if valid
|
| 176 |
+
if "error" not in result and result.get("risk") != "INVALID":
|
| 177 |
+
record = {
|
| 178 |
+
"time": datetime.now().strftime("%H:%M:%S"),
|
| 179 |
+
"patient": patient,
|
| 180 |
+
"age": age,
|
| 181 |
+
"task": task.capitalize(),
|
| 182 |
+
"diagnosis": result["prediction"]["label"],
|
| 183 |
+
"risk": result["risk"]
|
| 184 |
+
}
|
| 185 |
+
HISTORY.insert(0, record) # Add to top
|
| 186 |
+
|
| 187 |
+
return result
|
| 188 |
+
|
| 189 |
+
@app.get("/history")
|
| 190 |
+
def get_history():
|
| 191 |
+
return HISTORY
|
| 192 |
+
|
| 193 |
+
@app.post("/reset_history")
|
| 194 |
+
def reset_history():
|
| 195 |
+
global HISTORY
|
| 196 |
+
HISTORY = []
|
| 197 |
+
return {"status": "cleared"}
|
| 198 |
|
| 199 |
@app.get("/", response_class=HTMLResponse)
|
| 200 |
def home():
|
|
|
|
| 222 |
</p>
|
| 223 |
</div>
|
| 224 |
</div>
|
|
|
|
| 225 |
<div class="flex flex-col items-end gap-2">
|
| 226 |
<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">
|
| 227 |
<option value="en">🇬🇧 English</option>
|
| 228 |
+
<option value="hi">🇮🇳 हिंदी</option>
|
|
|
|
| 229 |
<option value="kha">🌲 Khasi</option>
|
| 230 |
<option value="gar">⛰️ Garo</option>
|
| 231 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
</div>
|
| 233 |
</div>
|
| 234 |
</nav>
|
|
|
|
| 263 |
</div>
|
| 264 |
|
| 265 |
<div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-100">
|
| 266 |
+
<h2 id="header-text" class="text-xl font-bold text-gray-700 mb-2 text-center">Select a Category</h2>
|
| 267 |
|
| 268 |
+
<div id="scope-box" class="hidden mb-6 text-center">
|
| 269 |
+
<p class="text-xs font-bold text-gray-400 uppercase mb-2" data-translate="lbl_scope">Scope of Detection</p>
|
| 270 |
+
<div id="scope-tags" class="flex flex-wrap justify-center gap-2"></div>
|
| 271 |
+
</div>
|
| 272 |
+
|
| 273 |
<div id="inputs" class="opacity-50 pointer-events-none transition-opacity mb-6">
|
| 274 |
<div class="grid grid-cols-2 gap-4 mb-4">
|
| 275 |
<div>
|
|
|
|
| 278 |
</div>
|
| 279 |
<div>
|
| 280 |
<label class="text-xs font-bold text-gray-400 uppercase" data-translate="lbl_age">Age / ID</label>
|
| 281 |
+
<input type="text" id="p-age" class="w-full border p-2 rounded bg-gray-50 outline-none focus:border-blue-500">
|
| 282 |
</div>
|
| 283 |
</div>
|
| 284 |
|
| 285 |
<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">
|
| 286 |
<input type="file" id="file-input" class="hidden" onchange="showPreview(event)" onclick="this.value=null">
|
|
|
|
| 287 |
<div id="placeholder" class="group-hover:scale-105 transition-transform">
|
| 288 |
<i id="upload-icon" class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
|
| 289 |
<p id="upload-text" class="text-gray-500 font-medium" data-translate="txt_upload">Tap to upload</p>
|
| 290 |
</div>
|
|
|
|
| 291 |
<img id="img-preview" class="hidden mx-auto max-h-56 rounded shadow object-contain">
|
| 292 |
<audio id="audio-preview" controls class="hidden w-full mt-4"></audio>
|
| 293 |
</div>
|
|
|
|
| 319 |
<span id="alert-text">--</span>
|
| 320 |
</div>
|
| 321 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
</div>
|
| 323 |
+
</div>
|
| 324 |
|
| 325 |
+
<div class="mt-8">
|
| 326 |
+
<div class="flex justify-between items-center mb-4">
|
| 327 |
+
<h3 class="text-lg font-bold text-gray-700 uppercase tracking-wide"><i class="fas fa-history mr-2"></i> Recent Patients</h3>
|
| 328 |
+
<button onclick="clearHistory()" class="text-xs text-red-500 hover:text-red-700 font-bold border border-red-200 px-3 py-1 rounded hover:bg-red-50">CLEAR HISTORY</button>
|
| 329 |
+
</div>
|
| 330 |
+
<div class="bg-white rounded-xl shadow overflow-hidden border border-gray-100">
|
| 331 |
+
<table class="w-full text-sm text-left">
|
| 332 |
+
<thead class="bg-gray-50 text-gray-500 font-bold uppercase text-xs">
|
| 333 |
+
<tr>
|
| 334 |
+
<th class="px-6 py-3">Time</th>
|
| 335 |
+
<th class="px-6 py-3">Patient</th>
|
| 336 |
+
<th class="px-6 py-3">Category</th>
|
| 337 |
+
<th class="px-6 py-3">Diagnosis</th>
|
| 338 |
+
<th class="px-6 py-3">Risk</th>
|
| 339 |
+
</tr>
|
| 340 |
+
</thead>
|
| 341 |
+
<tbody id="history-table" class="divide-y divide-gray-100">
|
| 342 |
+
<tr class="text-gray-400 text-center"><td colspan="5" class="py-4">No records yet.</td></tr>
|
| 343 |
+
</tbody>
|
| 344 |
+
</table>
|
| 345 |
+
</div>
|
| 346 |
</div>
|
| 347 |
</div>
|
| 348 |
|
| 349 |
<script>
|
| 350 |
+
const MODEL_SCOPES = {
|
| 351 |
+
lungs: ["Pneumonia", "Tuberculosis", "Viral Infection", "Normal Lung"],
|
| 352 |
+
cough: ["COPD Signs", "Whooping Cough", "Wheezing", "Respiratory Infection"],
|
| 353 |
+
fracture: ["Bone Fracture", "Dislocation", "Healthy Bone Structure"],
|
| 354 |
+
brain: ["Glioma Tumor", "Meningioma Tumor", "Pituitary Tumor", "No Tumor"],
|
| 355 |
+
eye: ["Cataract", "Diabetic Retinopathy", "Glaucoma", "Normal Eye"],
|
| 356 |
+
skin: ["Melanoma", "Basal Cell Carcinoma", "Nevus (Mole)", "Benign Keratosis"]
|
| 357 |
+
};
|
| 358 |
+
|
| 359 |
const TRANSLATIONS = {
|
| 360 |
en: {
|
| 361 |
govt_title: "GOVERNMENT OF MEGHALAYA",
|
|
|
|
| 367 |
btn_run: "Run Diagnosis",
|
| 368 |
txt_analyzing: "Downloading AI Model & Analyzing...",
|
| 369 |
lbl_result: "Analysis Result", lbl_conf: "Confidence",
|
| 370 |
+
lbl_action: "Action Required", lbl_scope: "Scope of Detection"
|
| 371 |
},
|
| 372 |
hi: {
|
| 373 |
govt_title: "मेघालय सरकार",
|
|
|
|
| 379 |
btn_run: "जांच शुरू करें",
|
| 380 |
txt_analyzing: "विश्लेषण कर रहा है...",
|
| 381 |
lbl_result: "परिणाम", lbl_conf: "विश्वास स्तर",
|
| 382 |
+
lbl_action: "आवश्यक कार्रवाई", lbl_scope: "जांच का दायरा"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
},
|
| 384 |
+
kha: {
|
| 385 |
govt_title: "SORKAR MEGHALAYA",
|
| 386 |
online_status: "Online: Shillong",
|
| 387 |
abha_link: "ABHA Link",
|
|
|
|
| 391 |
btn_run: "Leh Test",
|
| 392 |
txt_analyzing: "Dang Check...",
|
| 393 |
lbl_result: "Result", lbl_conf: "Jingshisha",
|
| 394 |
+
lbl_action: "Leh ia kane", lbl_scope: "Jingshem Jong Ka Test"
|
| 395 |
},
|
| 396 |
+
gar: {
|
| 397 |
govt_title: "MEGHALAYA SORKARI",
|
| 398 |
online_status: "Online: Shillong",
|
| 399 |
abha_link: "ABHA Link",
|
|
|
|
| 403 |
btn_run: "Porikka Ka'bo",
|
| 404 |
txt_analyzing: "Niyenga...",
|
| 405 |
lbl_result: "Result", lbl_conf: "Bebera'ani",
|
| 406 |
+
lbl_action: "Nangchongmotgipa Kam", lbl_scope: "Am·sandiani"
|
| 407 |
}
|
| 408 |
};
|
| 409 |
|
|
|
|
| 412 |
let currFile = null;
|
| 413 |
let currLang = 'en';
|
| 414 |
|
| 415 |
+
// Load History on Start
|
| 416 |
+
updateHistoryTable();
|
| 417 |
+
|
| 418 |
function changeLanguage() {
|
| 419 |
currLang = document.getElementById('lang-select').value;
|
| 420 |
let t = TRANSLATIONS[currLang];
|
|
|
|
| 421 |
document.querySelectorAll('[data-translate]').forEach(el => {
|
| 422 |
let key = el.getAttribute('data-translate');
|
| 423 |
if (t[key]) el.innerText = t[key];
|
| 424 |
});
|
|
|
|
|
|
|
| 425 |
if (currTask) {
|
| 426 |
let taskKey = "btn_" + currTask;
|
| 427 |
document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${t[taskKey]}</span>`;
|
|
|
|
| 433 |
currType = type;
|
| 434 |
let t = TRANSLATIONS[currLang];
|
| 435 |
|
|
|
|
| 436 |
document.querySelectorAll('button[id^="btn-"]').forEach(b => b.classList.remove('ring-2', 'ring-blue-400', 'border-blue-500', 'bg-blue-50'));
|
| 437 |
let btn = document.getElementById('btn-'+task);
|
| 438 |
btn.classList.add('ring-2', 'ring-blue-400', 'border-blue-500', 'bg-blue-50');
|
| 439 |
|
|
|
|
| 440 |
let taskName = t["btn_" + task];
|
| 441 |
document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${taskName}</span>`;
|
| 442 |
+
|
| 443 |
+
let scopeBox = document.getElementById('scope-box');
|
| 444 |
+
let scopeTags = document.getElementById('scope-tags');
|
| 445 |
+
scopeTags.innerHTML = "";
|
| 446 |
+
MODEL_SCOPES[task].forEach(disease => {
|
| 447 |
+
let tag = document.createElement("span");
|
| 448 |
+
tag.className = "px-2 py-1 bg-blue-50 text-blue-800 text-xs rounded border border-blue-100 font-semibold";
|
| 449 |
+
tag.innerText = disease;
|
| 450 |
+
scopeTags.appendChild(tag);
|
| 451 |
+
});
|
| 452 |
+
scopeBox.classList.remove('hidden');
|
| 453 |
+
|
| 454 |
let input = document.getElementById('file-input');
|
| 455 |
let icon = document.getElementById('upload-icon');
|
| 456 |
let text = document.getElementById('upload-text');
|
|
|
|
| 464 |
}
|
| 465 |
text.innerText = t['txt_upload'];
|
| 466 |
|
|
|
|
| 467 |
document.getElementById('inputs').classList.remove('opacity-50', 'pointer-events-none');
|
| 468 |
document.getElementById('result-box').classList.add('hidden');
|
| 469 |
document.getElementById('run-btn').classList.add('hidden');
|
|
|
|
| 477 |
if (event.target.files && event.target.files[0]) {
|
| 478 |
currFile = event.target.files[0];
|
| 479 |
let url = URL.createObjectURL(currFile);
|
|
|
|
| 480 |
if (currType === 'audio') {
|
| 481 |
let aud = document.getElementById('audio-preview');
|
| 482 |
aud.src = url;
|
|
|
|
| 488 |
img.classList.remove('hidden');
|
| 489 |
document.getElementById('audio-preview').classList.add('hidden');
|
| 490 |
}
|
|
|
|
| 491 |
document.getElementById('placeholder').classList.add('hidden');
|
| 492 |
document.getElementById('run-btn').classList.remove('hidden');
|
| 493 |
document.getElementById('result-box').classList.add('hidden');
|
|
|
|
| 496 |
|
| 497 |
async function analyze() {
|
| 498 |
if (!currTask || !currFile) return;
|
| 499 |
+
let pname = document.getElementById('p-name').value;
|
| 500 |
+
let page = document.getElementById('p-age').value;
|
| 501 |
+
if (!pname) { alert("Please enter Patient Name."); return; }
|
| 502 |
|
| 503 |
document.getElementById('run-btn').classList.add('hidden');
|
| 504 |
document.getElementById('loader').classList.remove('hidden');
|
|
|
|
| 507 |
let formData = new FormData();
|
| 508 |
formData.append("file", currFile);
|
| 509 |
|
| 510 |
+
// Pass patient details to backend to save history
|
| 511 |
try {
|
| 512 |
+
let url = `/predict/${currTask}?patient=${encodeURIComponent(pname)}&age=${encodeURIComponent(page)}`;
|
| 513 |
+
let res = await fetch(url, { method: "POST", body: formData });
|
| 514 |
let data = await res.json();
|
| 515 |
|
| 516 |
document.getElementById('loader').classList.add('hidden');
|
| 517 |
document.getElementById('result-box').classList.remove('hidden');
|
| 518 |
+
|
| 519 |
+
// Refresh History Table
|
| 520 |
+
updateHistoryTable();
|
| 521 |
|
| 522 |
if (data.risk === "INVALID") {
|
| 523 |
document.getElementById('res-label').innerText = "Rejected";
|
|
|
|
| 543 |
document.getElementById('alert-box').classList.add('hidden');
|
| 544 |
}
|
| 545 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
} catch (e) {
|
| 547 |
alert("Connection Failed.");
|
| 548 |
console.error(e);
|
|
|
|
| 551 |
}
|
| 552 |
}
|
| 553 |
|
| 554 |
+
async function updateHistoryTable() {
|
| 555 |
+
try {
|
| 556 |
+
let res = await fetch("/history");
|
| 557 |
+
let data = await res.json();
|
| 558 |
+
let tbody = document.getElementById('history-table');
|
| 559 |
+
tbody.innerHTML = "";
|
| 560 |
+
|
| 561 |
+
if(data.length === 0) {
|
| 562 |
+
tbody.innerHTML = '<tr class="text-gray-400 text-center"><td colspan="5" class="py-4">No records yet.</td></tr>';
|
| 563 |
+
return;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
data.forEach(row => {
|
| 567 |
+
let color = row.risk === "HIGH" ? "text-red-600 font-bold" : row.risk === "MODERATE" ? "text-yellow-600 font-bold" : "text-green-600";
|
| 568 |
+
let tr = `
|
| 569 |
+
<tr class="bg-white border-b hover:bg-gray-50">
|
| 570 |
+
<td class="px-6 py-4 text-gray-500">${row.time}</td>
|
| 571 |
+
<td class="px-6 py-4 font-bold text-gray-800">${row.patient}</td>
|
| 572 |
+
<td class="px-6 py-4 text-gray-600">${row.task}</td>
|
| 573 |
+
<td class="px-6 py-4 text-gray-800">${row.diagnosis}</td>
|
| 574 |
+
<td class="px-6 py-4 ${color}">${row.risk}</td>
|
| 575 |
+
</tr>
|
| 576 |
+
`;
|
| 577 |
+
tbody.innerHTML += tr;
|
| 578 |
+
});
|
| 579 |
+
} catch(e) { console.error("History fetch failed"); }
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
async function clearHistory() {
|
| 583 |
+
if(confirm("Delete all history?")) {
|
| 584 |
+
await fetch("/reset_history", { method: "POST" });
|
| 585 |
+
updateHistoryTable();
|
| 586 |
+
}
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
function updateBadge(text, bg, color) {
|
| 590 |
let b = document.getElementById('res-badge');
|
| 591 |
b.innerText = text;
|