serene-abyss commited on
Commit
93e5acb
·
verified ·
1 Parent(s): 934e365

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +365 -0
app.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, HTTPException
7
+ from fastapi.responses import HTMLResponse
8
+ import io
9
+ import os
10
+ import gc
11
+
12
+ # ==========================================
13
+ # 1. CONFIGURATION (Rural India Context)
14
+ # ==========================================
15
+ MODELS = {
16
+ "lungs": {
17
+ "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
18
+ "desc": "Tuberculosis & Pneumonia (Chest X-Ray)",
19
+ "safe": ["NORMAL", "normal", "No Pneumonia"],
20
+ "check_color": True # X-Rays must be black & white
21
+ },
22
+ "blood": {
23
+ "id": "mrm8488/vit-base-patch16-224-finetuned-malaria-detection",
24
+ "desc": "Malaria Screening (Microscopic Slide)",
25
+ "safe": ["Uninfected", "uninfected"],
26
+ "check_color": False
27
+ },
28
+ "eye": {
29
+ "id": "AventIQ-AI/resnet18-cataract-detection-system",
30
+ "desc": "Cataract Detection (Smartphone Eye Photo)",
31
+ "safe": ["Normal", "normal", "healthy"],
32
+ "check_color": False
33
+ },
34
+ "skin": {
35
+ "id": "Anwarkh1/Skin_Cancer-Image_Classification",
36
+ "desc": "Dermatology & Lesion Analysis",
37
+ "safe": ["Benign", "benign", "nv", "bkl"],
38
+ "check_color": False
39
+ }
40
+ }
41
+
42
+ # ==========================================
43
+ # 2. AI ENGINE (The Brain)
44
+ # ==========================================
45
+ class MedicalEngine:
46
+ def __init__(self):
47
+ # Force CPU to avoid memory crashes on Free Tier
48
+ self.device = "cpu"
49
+ print("✅ System Initialized: Medical Engine Ready (Lazy Loading)")
50
+
51
+ # Standard Image Transformation
52
+ self.transform = transforms.Compose([
53
+ transforms.Resize((224, 224)),
54
+ transforms.ToTensor(),
55
+ transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
56
+ ])
57
+
58
+ def validate_image(self, image, task):
59
+ """Guardrail: Rejects obvious bad images (e.g. Selfies for X-Ray)"""
60
+ if MODELS[task]["check_color"]:
61
+ # Check saturation for X-Rays
62
+ stat = ImageStat.Stat(image.convert('HSV'))
63
+ saturation = stat.mean[1] # 0 = Gray, 255 = Color
64
+ if saturation > 35: # Threshold
65
+ return False, "⚠️ Invalid Image: This looks like a color photo. Please upload a Black & White X-Ray."
66
+ return True, ""
67
+
68
+ def predict(self, image_bytes, task):
69
+ # A. Validation
70
+ try:
71
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
72
+ except:
73
+ return {"error": "File is not a valid image."}
74
+
75
+ is_valid, msg = self.validate_image(image, task)
76
+ if not is_valid:
77
+ return {"error": msg, "risk": "INVALID"}
78
+
79
+ # B. Load Model (On Demand)
80
+ print(f"⏳ Downloading/Loading Model for: {task}...")
81
+ try:
82
+ model_id = MODELS[task]["id"]
83
+ model = AutoModelForImageClassification.from_pretrained(model_id)
84
+ model.to(self.device)
85
+ model.eval()
86
+ except Exception as e:
87
+ print(f"❌ Model Load Error: {e}")
88
+ return {"error": "Failed to load AI model. Try again."}
89
+
90
+ # C. Inference
91
+ try:
92
+ inputs = self.transform(image).unsqueeze(0).to(self.device)
93
+ with torch.no_grad():
94
+ outputs = model(inputs)
95
+ probs = F.softmax(outputs.logits, dim=-1)
96
+
97
+ # Extract Results
98
+ results = []
99
+ for i, score in enumerate(probs[0]):
100
+ label = model.config.id2label[i]
101
+ results.append({"label": label, "score": float(score)})
102
+ results.sort(key=lambda x: x['score'], reverse=True)
103
+
104
+ top = results[0]
105
+
106
+ # D. Risk Logic
107
+ safe_words = MODELS[task]["safe"]
108
+ is_safe = any(s.lower() in top["label"].lower() for s in safe_words)
109
+
110
+ if top["score"] < 0.5:
111
+ risk = "UNCERTAIN" # Low confidence
112
+ elif is_safe:
113
+ risk = "LOW"
114
+ else:
115
+ risk = "HIGH" if top["score"] > 0.75 else "MODERATE"
116
+
117
+ except Exception as e:
118
+ return {"error": f"Prediction Error: {str(e)}"}
119
+
120
+ finally:
121
+ # E. Cleanup RAM (Critical for Free Tier)
122
+ del model
123
+ gc.collect()
124
+
125
+ return {
126
+ "task": task,
127
+ "desc": MODELS[task]["desc"],
128
+ "prediction": top,
129
+ "risk": risk
130
+ }
131
+
132
+ # ==========================================
133
+ # 3. API SERVER (FastAPI)
134
+ # ==========================================
135
+ app = FastAPI()
136
+ engine = MedicalEngine()
137
+
138
+ @app.post("/predict/{task}")
139
+ async def predict_route(task: str, file: UploadFile = File(...)):
140
+ if task not in MODELS:
141
+ return {"error": "Invalid Task"}
142
+
143
+ content = await file.read()
144
+ return engine.predict(content, task)
145
+
146
+ # ==========================================
147
+ # 4. FRONTEND UI (HTML embedded)
148
+ # ==========================================
149
+ @app.get("/", response_class=HTMLResponse)
150
+ def home():
151
+ return """
152
+ <!DOCTYPE html>
153
+ <html>
154
+ <head>
155
+ <title>MediScan Rural | Ayushman Bharat Node</title>
156
+ <script src="https://cdn.tailwindcss.com"></script>
157
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
158
+ </head>
159
+ <body class="bg-slate-50 min-h-screen flex flex-col font-sans">
160
+
161
+ <nav class="bg-blue-900 text-white p-4 shadow-xl">
162
+ <div class="container mx-auto flex justify-between items-center">
163
+ <div class="flex items-center gap-3">
164
+ <i class="fas fa-heartbeat text-2xl text-green-400"></i>
165
+ <div>
166
+ <h1 class="text-xl font-bold">MediScan Rural</h1>
167
+ <p class="text-xs text-blue-200">North East Node • Online</p>
168
+ </div>
169
+ </div>
170
+ <div class="text-right hidden md:block">
171
+ <span class="bg-blue-800 px-3 py-1 rounded text-xs font-bold border border-blue-600">
172
+ <i class="fas fa-satellite-dish mr-1"></i> Ayushman Bharat Linked
173
+ </span>
174
+ </div>
175
+ </div>
176
+ </nav>
177
+
178
+ <div class="container mx-auto mt-8 p-4 max-w-3xl flex-grow">
179
+
180
+ <div class="grid grid-cols-4 gap-2 mb-6">
181
+ <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">
182
+ <i class="fas fa-lungs text-blue-500 block text-2xl mb-1"></i> Lungs
183
+ </button>
184
+ <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">
185
+ <i class="fas fa-burn text-red-500 block text-2xl mb-1"></i> Malaria
186
+ </button>
187
+ <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">
188
+ <i class="fas fa-eye text-indigo-500 block text-2xl mb-1"></i> Eye
189
+ </button>
190
+ <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">
191
+ <i class="fas fa-hand-dots text-orange-500 block text-2xl mb-1"></i> Skin
192
+ </button>
193
+ </div>
194
+
195
+ <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200">
196
+ <h2 id="header-text" class="text-xl font-bold text-gray-700 mb-4">Select a Category above</h2>
197
+
198
+ <div id="inputs" class="opacity-50 pointer-events-none transition-opacity mb-6">
199
+ <div class="grid grid-cols-2 gap-4 mb-4">
200
+ <input type="text" id="p-name" placeholder="Patient Name" class="border p-2 rounded bg-gray-50">
201
+ <input type="text" placeholder="Age / ID" class="border p-2 rounded bg-gray-50">
202
+ </div>
203
+
204
+ <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">
205
+ <input type="file" id="file-input" class="hidden" accept="image/*" onchange="showPreview(event)" onclick="this.value=null">
206
+ <div id="placeholder">
207
+ <i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-2"></i>
208
+ <p class="text-gray-500 text-sm">Tap to upload image</p>
209
+ </div>
210
+ <img id="preview" class="hidden mx-auto max-h-48 rounded shadow">
211
+ </div>
212
+ </div>
213
+
214
+ <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">
215
+ <i class="fas fa-microscope mr-2"></i> Run Diagnosis
216
+ </button>
217
+
218
+ <div id="loader" class="hidden text-center py-6">
219
+ <div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"></div>
220
+ <p class="text-sm text-gray-500 mt-2 font-semibold">Downloading AI Model & Analyzing...</p>
221
+ <p class="text-xs text-gray-400">(This takes ~20s on first run)</p>
222
+ </div>
223
+
224
+ <div id="result-box" class="hidden mt-6 border-t pt-6">
225
+ <div class="flex justify-between items-start">
226
+ <div>
227
+ <p class="text-xs font-bold text-gray-400 uppercase">Prediction</p>
228
+ <h1 id="res-label" class="text-3xl font-extrabold text-gray-800">--</h1>
229
+ <p class="text-sm text-gray-600 mt-1">Confidence: <span id="res-conf" class="font-mono font-bold">--</span></p>
230
+ </div>
231
+ <span id="res-badge" class="px-3 py-1 rounded text-sm font-bold uppercase">--</span>
232
+ </div>
233
+
234
+ <div id="alert-box" class="hidden mt-4 p-3 bg-red-50 text-red-800 rounded border border-red-200 text-sm">
235
+ <i class="fas fa-ambulance mr-2"></i> <span id="alert-text">High Risk Detected.</span>
236
+ </div>
237
+
238
+ <div class="mt-4 bg-gray-50 p-3 rounded border flex items-center justify-between text-xs">
239
+ <span class="font-bold text-gray-500"><i class="fas fa-database mr-1"></i> Govt Sync</span>
240
+ <span id="sync-msg" class="text-yellow-600"><i class="fas fa-sync fa-spin"></i> Pending...</span>
241
+ </div>
242
+ </div>
243
+
244
+ </div>
245
+ </div>
246
+
247
+ <footer class="text-center p-4 text-xs text-gray-400">
248
+ ⚠️ Disclaimer: AI Research Tool. Verify with a Doctor.
249
+ </footer>
250
+
251
+ <script>
252
+ let currTask = null;
253
+ let currFile = null;
254
+
255
+ function setTask(task) {
256
+ currTask = task;
257
+
258
+ // Highlight Buttons
259
+ document.querySelectorAll('button[id^="btn-"]').forEach(b => b.classList.remove('ring-2', 'ring-blue-400', 'border-blue-500'));
260
+ document.getElementById('btn-'+task).classList.add('ring-2', 'ring-blue-400', 'border-blue-500');
261
+
262
+ // Reset UI
263
+ document.getElementById('header-text').innerHTML = `Upload <span class="uppercase text-blue-600">${task}</span> Image`;
264
+ document.getElementById('inputs').classList.remove('opacity-50', 'pointer-events-none');
265
+ document.getElementById('result-box').classList.add('hidden');
266
+ document.getElementById('run-btn').classList.add('hidden');
267
+ document.getElementById('placeholder').classList.remove('hidden');
268
+ document.getElementById('preview').classList.add('hidden');
269
+ currFile = null;
270
+ }
271
+
272
+ function showPreview(event) {
273
+ if (event.target.files && event.target.files[0]) {
274
+ currFile = event.target.files[0];
275
+ let reader = new FileReader();
276
+ reader.onload = function(e) {
277
+ document.getElementById('preview').src = e.target.result;
278
+ document.getElementById('preview').classList.remove('hidden');
279
+ document.getElementById('placeholder').classList.add('hidden');
280
+ document.getElementById('run-btn').classList.remove('hidden');
281
+ document.getElementById('result-box').classList.add('hidden');
282
+ };
283
+ reader.readAsDataURL(currFile);
284
+ }
285
+ }
286
+
287
+ async function analyze() {
288
+ if (!currTask || !currFile) return;
289
+
290
+ if (!document.getElementById('p-name').value) {
291
+ alert("Please enter Patient Name.");
292
+ return;
293
+ }
294
+
295
+ // Show Loader
296
+ document.getElementById('run-btn').classList.add('hidden');
297
+ document.getElementById('loader').classList.remove('hidden');
298
+ document.getElementById('result-box').classList.add('hidden');
299
+
300
+ let formData = new FormData();
301
+ formData.append("file", currFile);
302
+
303
+ try {
304
+ // Determine API URL (Handles Hugging Face URL structure)
305
+ let url = "/predict/" + currTask;
306
+
307
+ let res = await fetch(url, { method: "POST", body: formData });
308
+ let data = await res.json();
309
+
310
+ if (data.error) {
311
+ alert("Error: " + data.error);
312
+ resetLoading();
313
+ return;
314
+ }
315
+
316
+ // Show Results
317
+ document.getElementById('loader').classList.add('hidden');
318
+ document.getElementById('result-box').classList.remove('hidden');
319
+
320
+ document.getElementById('res-label').innerText = data.prediction.label;
321
+ document.getElementById('res-conf').innerText = (data.prediction.score * 100).toFixed(1) + "%";
322
+
323
+ let badge = document.getElementById('res-badge');
324
+ let alertBox = document.getElementById('alert-box');
325
+
326
+ if (data.risk === "HIGH") {
327
+ badge.className = "px-3 py-1 rounded text-sm font-bold uppercase bg-red-100 text-red-700";
328
+ alertBox.classList.remove('hidden');
329
+ document.getElementById('alert-text').innerText = "High Risk. Immediate Referral Recommended.";
330
+ } else if (data.risk === "MODERATE") {
331
+ badge.className = "px-3 py-1 rounded text-sm font-bold uppercase bg-yellow-100 text-yellow-700";
332
+ alertBox.classList.remove('hidden');
333
+ document.getElementById('alert-text').innerText = "Moderate Risk. Consult Doctor.";
334
+ } else if (data.risk === "INVALID" || data.risk === "UNCERTAIN") {
335
+ badge.className = "px-3 py-1 rounded text-sm font-bold uppercase bg-gray-200 text-gray-700";
336
+ alertBox.classList.remove('hidden');
337
+ document.getElementById('alert-text').innerText = "Image Unclear / Invalid. Retake Photo.";
338
+ } else {
339
+ badge.className = "px-3 py-1 rounded text-sm font-bold uppercase bg-green-100 text-green-700";
340
+ alertBox.classList.add('hidden');
341
+ }
342
+
343
+ badge.innerText = data.risk + " RISK";
344
+
345
+ // Sync Animation
346
+ setTimeout(() => {
347
+ document.getElementById('sync-msg').innerHTML = "<i class='fas fa-check-circle'></i> Synced!";
348
+ document.getElementById('sync-msg').className = "text-green-600 font-bold";
349
+ }, 2000);
350
+
351
+ } catch (e) {
352
+ alert("Connection Failed. Refresh and try again.");
353
+ console.error(e);
354
+ resetLoading();
355
+ }
356
+ }
357
+
358
+ function resetLoading() {
359
+ document.getElementById('loader').classList.add('hidden');
360
+ document.getElementById('run-btn').classList.remove('hidden');
361
+ }
362
+ </script>
363
+ </body>
364
+ </html>
365
+ """