kodetr commited on
Commit
9cdc954
·
verified ·
1 Parent(s): 2d7ab3a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +958 -279
app.py CHANGED
@@ -15,6 +15,641 @@ import uvicorn
15
 
16
  warnings.filterwarnings("ignore")
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  # ============================================================
19
  # 1. LOAD MODEL (with Hugging Face compatibility)
20
  # ============================================================
@@ -22,46 +657,29 @@ print("=" * 60)
22
  print("🚀 LOADING MODEL FOR HUGGING FACE SPACES")
23
  print("=" * 60)
24
 
25
- # Cek apakah model ada di root atau folder
26
- MODEL_PATHS = [
27
- "model.keras",
28
- "./model.keras",
29
- "/tmp/model.keras"
30
- ]
31
 
32
  best_model = None
33
  for model_path in MODEL_PATHS:
34
  if os.path.exists(model_path):
35
  try:
36
- print(f"📂 Trying to load model from: {model_path}")
37
- best_model = tf.keras.models.load_model(
38
- model_path,
39
- compile=False,
40
- safe_mode=False # Important for compatibility
41
- )
42
- print(f"✅ Model loaded successfully from {model_path}")
43
  break
44
  except Exception as e:
45
- print(f"❌ Failed to load from {model_path}: {e}")
46
 
47
- # Jika model tidak ditemukan, buat dummy model
48
  if best_model is None:
49
- print("⚠️ No model file found. Creating dummy model for demo...")
50
  from tensorflow.keras import layers, Model
51
  inputs = layers.Input(shape=(224, 224, 3))
52
  x = layers.GlobalAveragePooling2D()(inputs)
53
  dr_output = layers.Dense(5, name="dr_head")(x)
54
  dme_output = layers.Dense(3, name="dme_head")(x)
55
  best_model = Model(inputs, {"dr_head": dr_output, "dme_head": dme_output})
56
- best_model.compile(optimizer="adam", loss="categorical_crossentropy")
57
  print("✅ Dummy model created")
58
 
59
- # Summary model (debug info)
60
- try:
61
- best_model.summary()
62
- except:
63
- print("ℹ️ Model loaded, summary not available")
64
-
65
  # ============================================================
66
  # 2. CONFIG
67
  # ============================================================
@@ -69,39 +687,39 @@ IMG_SIZE = 224
69
  DR_CLASSES = ["No DR", "Mild", "Moderate", "Severe", "Proliferative DR"]
70
  DME_CLASSES = ["No DME", "Low Risk", "High Risk"]
71
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  # ============================================================
73
- # 3. PREPROCESSING FUNCTIONS
74
  # ============================================================
75
  def preprocess_pil_image(img):
76
- """Preprocess PIL Image for prediction"""
77
  if img.mode != 'RGB':
78
  img = img.convert('RGB')
79
  img = img.resize((IMG_SIZE, IMG_SIZE))
80
  arr = np.array(img, dtype=np.float32) / 255.0
81
  return np.expand_dims(arr, 0)
82
 
83
- # ============================================================
84
- # 4. SOFTMAX SAFETY
85
- # ============================================================
86
  def ensure_probability(x):
87
  x = np.asarray(x, dtype=np.float32)
88
  if x.min() < 0 or x.max() > 1.0 or abs(x.sum() - 1.0) > 1e-3:
89
  x = tf.nn.softmax(x).numpy()
90
  return x
91
 
92
- # ============================================================
93
- # 5. CORE PREDICTION FUNCTION
94
- # ============================================================
95
  def predict_image(image):
96
- """Core prediction function that returns structured data"""
97
  try:
98
- # Preprocess
99
  img_tensor = preprocess_pil_image(image)
100
-
101
- # Predict
102
  preds = best_model.predict(img_tensor, verbose=0)
103
 
104
- # Handle different model output formats
105
  dr_pred = None
106
  dme_pred = None
107
 
@@ -135,22 +753,17 @@ def predict_image(image):
135
  dr_pred = preds[:5]
136
  dme_pred = preds[5:8]
137
 
138
- # Take first batch if batch dimension exists
139
  if dr_pred is not None and len(dr_pred.shape) > 1:
140
  dr_pred = dr_pred[0]
141
  if dme_pred is not None and len(dme_pred.shape) > 1:
142
  dme_pred = dme_pred[0]
143
 
144
- if dr_pred is None:
145
- dr_pred = np.zeros(5)
146
- if dme_pred is None:
147
- dme_pred = np.zeros(3)
148
 
149
- # Apply softmax
150
  dr_probs = ensure_probability(dr_pred)
151
  dme_probs = ensure_probability(dme_pred)
152
 
153
- # Get results
154
  dr_idx = int(np.argmax(dr_probs))
155
  dme_idx = int(np.argmax(dme_probs))
156
 
@@ -160,298 +773,371 @@ def predict_image(image):
160
  dr_conf = float(dr_probs[dr_idx] * 100)
161
  dme_conf = float(dme_probs[dme_idx] * 100)
162
 
163
- # Generate recommendations
164
- if dr_name in ["No DR"]:
165
- rec_dr = "Lanjutkan pola hidup sehat dan lakukan pemeriksaan mata rutin minimal 1 tahun sekali."
166
- elif dr_name in ["Mild", "Moderate"]:
167
- rec_dr = "Disarankan kontrol gula darah secara ketat dan pemeriksaan mata berkala setiap 6 bulan."
168
- else: # Severe / Proliferative
169
- rec_dr = "Disarankan segera konsultasi ke dokter spesialis mata untuk evaluasi dan penanganan lebih lanjut."
170
-
171
- if dme_name == "No DME":
172
- rec_dme = "Belum ditemukan tanda edema makula diabetik, lanjutkan pemantauan rutin."
173
- elif dme_name == "Low Risk":
174
- rec_dme = "Perlu observasi ketat dan pemeriksaan lanjutan untuk mencegah progresivitas."
175
- else: # High Risk
176
- rec_dme = "Disarankan segera mendapatkan evaluasi klinis dan terapi oleh dokter spesialis mata."
177
 
178
  return {
179
  "success": True,
180
- "predictions": {
181
- "diabetic_retinopathy": {
182
- "classification": dr_name,
183
- "confidence": dr_conf,
184
- "index": dr_idx,
185
- "probabilities": dr_probs.tolist(),
186
- "recommendation": rec_dr
187
- },
188
- "diabetic_macular_edema": {
189
- "classification": dme_name,
190
- "confidence": dme_conf,
191
- "index": dme_idx,
192
- "probabilities": dme_probs.tolist(),
193
- "recommendation": rec_dme
194
- }
195
  }
196
  }
197
 
198
  except Exception as e:
199
- return {
200
- "success": False,
201
- "error": str(e)
202
- }
203
 
204
  # ============================================================
205
- # 6. CREATE FASTAPI APP
206
- # ============================================================
207
- app = FastAPI(
208
- title="DR & DME Detection API",
209
- description="API untuk mendeteksi Diabetic Retinopathy dan Diabetic Macular Edema",
210
- version="1.0.0"
211
- )
212
-
213
- # Enable CORS for mobile access
214
- app.add_middleware(
215
- CORSMiddleware,
216
- allow_origins=["*"],
217
- allow_credentials=True,
218
- allow_methods=["*"],
219
- allow_headers=["*"],
220
- )
221
-
222
- # ============================================================
223
- # 7. GRADIO UI FUNCTIONS
224
  # ============================================================
225
  def format_prediction_html(result):
226
- """Format prediction result as HTML for Gradio"""
227
  if not result["success"]:
228
  return f"""
229
- <div style="color: red; padding: 20px; border: 2px solid red; border-radius: 10px;">
230
- <h3>❌ Error</h3>
231
- <p>{result['error']}</p>
 
 
 
 
 
 
 
232
  </div>
233
  """
234
 
235
- preds = result["predictions"]
236
- dr = preds["diabetic_retinopathy"]
237
- dme = preds["diabetic_macular_edema"]
238
-
239
- dr_color = {
240
- "No DR": "#28a745",
241
- "Mild": "#ffc107",
242
- "Moderate": "#fd7e14",
243
- "Severe": "#dc3545",
244
- "Proliferative DR": "#6f42c1"
245
- }.get(dr["classification"], "#000000")
246
-
247
- dme_color = {
248
- "No DME": "#28a745",
249
- "Low Risk": "#ffc107",
250
- "High Risk": "#dc3545"
251
- }.get(dme["classification"], "#000000")
252
 
253
- html = f"""
254
- <div style="font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto;">
255
- <div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
256
- color: white; padding: 25px; border-radius: 15px 15px 0 0; margin-bottom: 20px;">
257
- <h1 style="margin: 0; font-size: 32px;">🔬 HASIL DETEKSI</h1>
258
- <p style="margin: 5px 0 0 0; font-size: 16px; opacity: 0.9;">AI-Powered Retina Analysis</p>
259
- </div>
260
-
261
- <div style="background: white; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden;">
262
- <table style="width: 100%; border-collapse: collapse;">
263
  <thead>
264
- <tr style="background-color: #f8f9fa;">
265
- <th style="padding: 16px; text-align: left; border-bottom: 2px solid #dee2e6; font-size: 18px;">Kondisi</th>
266
- <th style="padding: 16px; text-align: left; border-bottom: 2px solid #dee2e6; font-size: 18px;">Klasifikasi</th>
267
- <th style="padding: 16px; text-align: left; border-bottom: 2px solid #dee2e6; font-size: 18px;">Tingkat Kepercayaan</th>
268
  </tr>
269
  </thead>
270
  <tbody>
271
  <tr>
272
- <td style="padding: 16px; border-bottom: 1px solid #dee2e6; font-weight: bold;">Diabetic Retinopathy (DR)</td>
273
- <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
274
- <span style="color: {dr_color}; font-weight: bold; font-size: 18px;">{dr['classification']}</span>
 
 
275
  </td>
276
- <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
277
- <div style="display: flex; align-items: center; gap: 10px;">
278
- <div style="flex-grow: 1; background: #e9ecef; height: 20px; border-radius: 10px; overflow: hidden;">
279
- <div style="width: {dr['confidence']}%; background: {dr_color}; height: 100%;"></div>
280
  </div>
281
- <span style="font-weight: bold; min-width: 60px;">{dr['confidence']:.1f}%</span>
282
  </div>
283
  </td>
284
  </tr>
285
  <tr>
286
- <td style="padding: 16px; border-bottom: 1px solid #dee2e6; font-weight: bold;">Diabetic Macular Edema (DME)</td>
287
- <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
288
- <span style="color: {dme_color}; font-weight: bold; font-size: 18px;">{dme['classification']}</span>
 
 
289
  </td>
290
- <td style="padding: 16px; border-bottom: 1px solid #dee2e6;">
291
- <div style="display: flex; align-items: center; gap: 10px;">
292
- <div style="flex-grow: 1; background: #e9ecef; height: 20px; border-radius: 10px; overflow: hidden;">
293
- <div style="width: {dme['confidence']}%; background: {dme_color}; height: 100%;"></div>
294
  </div>
295
- <span style="font-weight: bold; min-width: 60px;">{dme['confidence']:.1f}%</span>
296
  </div>
297
  </td>
298
  </tr>
299
  </tbody>
300
  </table>
301
- </div>
302
-
303
- <div style="margin-top: 25px; background: white; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden;">
304
- <div style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; padding: 15px;">
305
- <h3 style="margin: 0; font-size: 22px;">🩺 REKOMENDASI KLINIS</h3>
306
- </div>
307
- <div style="padding: 20px;">
308
- <div style="margin-bottom: 15px;">
309
- <h4 style="color: #333; margin-bottom: 8px;">• Diabetic Retinopathy (DR):</h4>
310
- <p style="margin: 0; color: #555; line-height: 1.6;">{dr['recommendation']}</p>
311
  </div>
312
- <div>
313
- <h4 style="color: #333; margin-bottom: 8px;">• Diabetic Macular Edema (DME):</h4>
314
- <p style="margin: 0; color: #555; line-height: 1.6;">{dme['recommendation']}</p>
 
 
 
 
 
 
315
  </div>
316
  </div>
317
- </div>
318
-
319
- <div style="margin-top: 20px; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; font-size: 14px;">
320
- <strong>⚠️ Disclaimer:</strong> Hasil ini merupakan prediksi AI dan bukan diagnosis medis. Konsultasikan dengan dokter spesialis mata untuk diagnosis yang akurat.
321
- </div>
322
  </div>
323
  """
324
- return html
325
 
326
  def gradio_predict(image):
327
- """Main function for Gradio UI"""
328
  if image is None:
329
- return "❌ Silakan unggah gambar fundus retina"
 
 
 
 
 
 
330
 
331
  result = predict_image(image)
332
  return format_prediction_html(result)
333
 
334
  # ============================================================
335
- # 8. MULTI TEST IMAGES
336
- # ============================================================
337
- TEST_IMAGES = [
338
- "IDRiD_001test.jpg",
339
- "IDRiD_004test.jpg",
340
- "IDRiD_005test.jpg",
341
- "IDRiD_006test.jpg",
342
- "IDRiD_007test.jpg",
343
- "IDRiD_008test.jpg",
344
- "IDRiD_009test.jpg",
345
- "IDRiD_010test.jpg",
346
- "IDRiD_011test.jpg",
347
- "IDRiD_012test.jpg",
348
- ]
349
-
350
- TEST_IMAGES = [[p] for p in TEST_IMAGES if os.path.exists(p)]
351
-
352
- # ============================================================
353
- # 9. CREATE GRADIO APP
354
  # ============================================================
355
  with gr.Blocks(
356
- title="DR & DME Detection",
357
- theme=gr.themes.Soft()
 
 
 
 
 
 
358
  ) as demo:
359
 
360
- gr.Markdown("""
361
- # 🩺 DETEKSI DIABETIC RETINOPATHY & DME
362
- ### Sistem AI untuk Analisis Citra Fundus Retina
363
-
364
- Upload gambar fundus retina untuk mendeteksi:
365
- - **Diabetic Retinopathy (DR)**: Kerusakan retina akibat diabetes
366
- - **Diabetic Macular Edema (DME)**: Pembengkakan di makula
 
 
 
367
  """)
368
 
369
- with gr.Row():
370
- with gr.Column(scale=1):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  image_input = gr.Image(
372
  type="pil",
373
- label="📤 Upload Gambar Retina",
374
- height=300
 
 
375
  )
376
 
377
- upload_btn = gr.Button(
378
- "🔍 Analisis Gambar",
379
- variant="primary",
380
- size="lg"
381
  )
382
 
383
- gr.Markdown("""
384
- **Format yang didukung:** JPG, PNG, JPEG
385
- **Ukuran rekomendasi:** 224×224 piksel
386
- **Warna:** RGB (akan dikonversi otomatis)
 
 
 
 
 
 
387
  """)
388
 
389
- with gr.Column(scale=2):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  output_html = gr.HTML(
391
- label="📊 Hasil Analisis",
392
- value="<div style='text-align: center; padding: 50px; color: #666;'>Hasil analisis akan muncul di sini setelah mengupload gambar.</div>"
393
  )
394
-
395
- gr.Markdown("### 🧪 Data Testing")
396
- gr.Examples(
397
- examples=TEST_IMAGES,
398
- inputs=image_input
399
- )
400
 
401
- # API Info section
402
- gr.Markdown("---")
403
- with gr.Accordion("📱 Akses API dari Mobile App", open=False):
404
- gr.Markdown("""
405
- ### API Endpoints:
406
-
407
- 1. **POST /api/predict** - Upload file gambar
408
- ```bash
409
- curl -X POST "https://kodetr-idrid.hf.space/api/predict" \\
410
- -F "file=@retina_image.jpg"
411
- ```
412
-
413
- 2. **GET /api/health** - Health check
414
-
415
- 3. **GET /api/info** - API info
416
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
- upload_btn.click(
419
- fn=gradio_predict,
420
  inputs=image_input,
421
- outputs=output_html
 
 
 
 
 
 
422
  )
423
 
424
  # ============================================================
425
- # 10. FASTAPI ENDPOINTS (under /api path)
426
  # ============================================================
427
- @app.get("/api/info")
428
- async def api_info():
429
- """API info endpoint"""
430
- return {
431
- "message": "DR & DME Detection API",
432
- "version": "1.0.0",
433
- "endpoints": {
434
- "docs": "/docs",
435
- "health": "/api/health",
436
- "predict": "/api/predict",
437
- "ui": "/" # Gradio UI at root
438
- }
439
- }
440
 
441
- @app.get("/api/health")
442
- async def health_check():
443
- """Health check endpoint"""
444
- return {
445
- "status": "healthy",
446
- "model_loaded": best_model is not None
447
- }
448
 
449
  @app.post("/api/predict")
450
- async def predict_endpoint(file: UploadFile = File(...)):
451
- """
452
- Predict endpoint for form-data file upload
453
- Accepts: image file (jpg, png, jpeg)
454
- """
455
  try:
456
  if not file.content_type.startswith('image/'):
457
  raise HTTPException(status_code=400, detail="File must be an image")
@@ -466,34 +1152,27 @@ async def predict_endpoint(file: UploadFile = File(...)):
466
 
467
  return JSONResponse(content=result)
468
 
469
- except HTTPException:
470
- raise
471
  except Exception as e:
472
  raise HTTPException(status_code=500, detail=str(e))
473
 
474
- # ============================================================
475
- # 11. MOUNT GRADIO TO ROOT PATH
476
- # ============================================================
477
- # Ini penting: Mount Gradio ke root path
478
  app = gr.mount_gradio_app(app, demo, path="/")
479
 
480
  # ============================================================
481
- # 11. MAIN ENTRY POINT
482
  # ============================================================
483
  if __name__ == "__main__":
484
  print("\n" + "="*60)
485
- print("🚀 SERVER STARTING")
486
  print("="*60)
487
- print(f"🖥️ Gradio UI: http://0.0.0.0:7860/")
488
- print(f"📱 API Docs: http://0.0.0.0:7860/docs")
489
- print(f"🏥 Health Check: http://0.0.0.0:7860/api/health")
490
- print(f"🔧 API Info: http://0.0.0.0:7860/api/info")
491
- print(f"📤 Predict: http://0.0.0.0:7860/api/predict")
492
  print("="*60)
493
 
494
- uvicorn.run(
495
- app,
496
- host="0.0.0.0",
497
- port=7860,
498
- log_level="info"
499
  )
 
15
 
16
  warnings.filterwarnings("ignore")
17
 
18
+ # ============================================================
19
+ # CUSTOM CSS FOR GRADIO UI
20
+ # ============================================================
21
+ CUSTOM_CSS = """
22
+ /* Reset and base styles */
23
+ * {
24
+ margin: 0;
25
+ padding: 0;
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ :root {
30
+ --primary-color: #4f46e5;
31
+ --primary-light: #6366f1;
32
+ --secondary-color: #10b981;
33
+ --danger-color: #ef4444;
34
+ --warning-color: #f59e0b;
35
+ --info-color: #3b82f6;
36
+ --dark-color: #1f2937;
37
+ --light-color: #f9fafb;
38
+ --gray-color: #6b7280;
39
+ --border-color: #e5e7eb;
40
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
41
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
42
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
43
+ --radius-sm: 0.375rem;
44
+ --radius-md: 0.5rem;
45
+ --radius-lg: 0.75rem;
46
+ --radius-xl: 1rem;
47
+ }
48
+
49
+ body {
50
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
51
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
52
+ min-height: 100vh;
53
+ color: var(--dark-color);
54
+ }
55
+
56
+ /* Override Gradio container */
57
+ .gradio-container {
58
+ max-width: 1400px !important;
59
+ margin: 0 auto !important;
60
+ padding: 2rem !important;
61
+ background: transparent !important;
62
+ }
63
+
64
+ /* Header styling */
65
+ .header-section {
66
+ text-align: center;
67
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
68
+ color: white;
69
+ padding: 3rem 2rem;
70
+ border-radius: var(--radius-xl);
71
+ margin-bottom: 3rem;
72
+ box-shadow: var(--shadow-lg);
73
+ position: relative;
74
+ overflow: hidden;
75
+ }
76
+
77
+ .header-section::before {
78
+ content: '';
79
+ position: absolute;
80
+ top: 0;
81
+ left: 0;
82
+ right: 0;
83
+ bottom: 0;
84
+ background: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23ffffff' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E");
85
+ opacity: 0.1;
86
+ }
87
+
88
+ .header-title {
89
+ font-size: 3rem;
90
+ font-weight: 800;
91
+ margin-bottom: 1rem;
92
+ line-height: 1.2;
93
+ position: relative;
94
+ z-index: 1;
95
+ }
96
+
97
+ .header-subtitle {
98
+ font-size: 1.25rem;
99
+ opacity: 0.9;
100
+ margin-bottom: 0.5rem;
101
+ position: relative;
102
+ z-index: 1;
103
+ }
104
+
105
+ .header-description {
106
+ font-size: 1rem;
107
+ opacity: 0.8;
108
+ max-width: 800px;
109
+ margin: 1.5rem auto 0;
110
+ line-height: 1.6;
111
+ position: relative;
112
+ z-index: 1;
113
+ }
114
+
115
+ /* Main content layout */
116
+ .content-wrapper {
117
+ display: grid;
118
+ grid-template-columns: 1fr 1.5fr;
119
+ gap: 2rem;
120
+ margin-bottom: 3rem;
121
+ }
122
+
123
+ @media (max-width: 1024px) {
124
+ .content-wrapper {
125
+ grid-template-columns: 1fr;
126
+ }
127
+ }
128
+
129
+ /* Upload section */
130
+ .upload-container {
131
+ background: white;
132
+ padding: 2rem;
133
+ border-radius: var(--radius-xl);
134
+ box-shadow: var(--shadow-md);
135
+ border: 1px solid var(--border-color);
136
+ }
137
+
138
+ .upload-header {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 0.75rem;
142
+ margin-bottom: 1.5rem;
143
+ color: var(--primary-color);
144
+ }
145
+
146
+ .upload-icon {
147
+ font-size: 1.5rem;
148
+ }
149
+
150
+ .upload-header h3 {
151
+ font-size: 1.5rem;
152
+ font-weight: 600;
153
+ }
154
+
155
+ .image-upload-area {
156
+ border: 2px dashed var(--border-color);
157
+ border-radius: var(--radius-lg);
158
+ padding: 2rem;
159
+ text-align: center;
160
+ margin-bottom: 1.5rem;
161
+ transition: all 0.3s ease;
162
+ background: var(--light-color);
163
+ cursor: pointer;
164
+ }
165
+
166
+ .image-upload-area:hover {
167
+ border-color: var(--primary-light);
168
+ background: rgba(99, 102, 241, 0.05);
169
+ }
170
+
171
+ .image-upload-area .upload-placeholder {
172
+ color: var(--gray-color);
173
+ font-size: 1rem;
174
+ }
175
+
176
+ .image-preview-container {
177
+ margin-top: 1rem;
178
+ border-radius: var(--radius-md);
179
+ overflow: hidden;
180
+ border: 1px solid var(--border-color);
181
+ }
182
+
183
+ /* Button styling */
184
+ .analyze-button {
185
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%) !important;
186
+ color: white !important;
187
+ border: none !important;
188
+ padding: 1rem 2rem !important;
189
+ font-size: 1.125rem !important;
190
+ font-weight: 600 !important;
191
+ border-radius: var(--radius-lg) !important;
192
+ cursor: pointer !important;
193
+ transition: all 0.3s ease !important;
194
+ width: 100% !important;
195
+ margin-top: 1rem !important;
196
+ box-shadow: var(--shadow-md) !important;
197
+ display: flex !important;
198
+ align-items: center !important;
199
+ justify-content: center !important;
200
+ gap: 0.5rem !important;
201
+ }
202
+
203
+ .analyze-button:hover {
204
+ transform: translateY(-2px) !important;
205
+ box-shadow: var(--shadow-lg) !important;
206
+ background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary-color) 100%) !important;
207
+ }
208
+
209
+ .analyze-button:active {
210
+ transform: translateY(0) !important;
211
+ }
212
+
213
+ /* Guide box */
214
+ .upload-guide {
215
+ background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%);
216
+ padding: 1.5rem;
217
+ border-radius: var(--radius-lg);
218
+ margin-top: 1.5rem;
219
+ border-left: 4px solid var(--info-color);
220
+ }
221
+
222
+ .upload-guide h4 {
223
+ color: var(--info-color);
224
+ margin-bottom: 0.75rem;
225
+ font-size: 1.125rem;
226
+ }
227
+
228
+ .upload-guide ul {
229
+ list-style: none;
230
+ padding: 0;
231
+ }
232
+
233
+ .upload-guide li {
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 0.5rem;
237
+ margin-bottom: 0.5rem;
238
+ color: var(--dark-color);
239
+ }
240
+
241
+ .upload-guide li::before {
242
+ content: '✓';
243
+ color: var(--secondary-color);
244
+ font-weight: bold;
245
+ }
246
+
247
+ /* Results section */
248
+ .results-container {
249
+ background: white;
250
+ padding: 2rem;
251
+ border-radius: var(--radius-xl);
252
+ box-shadow: var(--shadow-md);
253
+ border: 1px solid var(--border-color);
254
+ height: 100%;
255
+ }
256
+
257
+ .results-header {
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 0.75rem;
261
+ margin-bottom: 1.5rem;
262
+ color: var(--primary-color);
263
+ }
264
+
265
+ .results-icon {
266
+ font-size: 1.5rem;
267
+ }
268
+
269
+ .results-header h3 {
270
+ font-size: 1.5rem;
271
+ font-weight: 600;
272
+ }
273
+
274
+ .results-placeholder {
275
+ text-align: center;
276
+ padding: 4rem 2rem;
277
+ color: var(--gray-color);
278
+ }
279
+
280
+ .results-placeholder h4 {
281
+ font-size: 1.25rem;
282
+ margin-bottom: 0.5rem;
283
+ color: var(--dark-color);
284
+ }
285
+
286
+ .arrow-indicator {
287
+ font-size: 3rem;
288
+ margin-top: 1rem;
289
+ color: var(--border-color);
290
+ }
291
+
292
+ /* Results content */
293
+ .results-content {
294
+ animation: fadeIn 0.5s ease;
295
+ }
296
+
297
+ @keyframes fadeIn {
298
+ from { opacity: 0; transform: translateY(10px); }
299
+ to { opacity: 1; transform: translateY(0); }
300
+ }
301
+
302
+ .results-title {
303
+ text-align: center;
304
+ margin-bottom: 2rem;
305
+ }
306
+
307
+ .results-title h2 {
308
+ font-size: 2rem;
309
+ color: var(--dark-color);
310
+ margin-bottom: 0.5rem;
311
+ }
312
+
313
+ .results-title p {
314
+ color: var(--gray-color);
315
+ font-size: 1rem;
316
+ }
317
+
318
+ /* Results table */
319
+ .results-table {
320
+ width: 100%;
321
+ border-collapse: separate;
322
+ border-spacing: 0;
323
+ margin: 2rem 0;
324
+ background: white;
325
+ border-radius: var(--radius-lg);
326
+ overflow: hidden;
327
+ box-shadow: var(--shadow-sm);
328
+ }
329
+
330
+ .results-table thead {
331
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
332
+ }
333
+
334
+ .results-table th {
335
+ padding: 1.25rem 1.5rem;
336
+ text-align: left;
337
+ font-weight: 600;
338
+ color: var(--dark-color);
339
+ border-bottom: 2px solid var(--border-color);
340
+ font-size: 1.1rem;
341
+ }
342
+
343
+ .results-table td {
344
+ padding: 1.25rem 1.5rem;
345
+ border-bottom: 1px solid var(--border-color);
346
+ }
347
+
348
+ .results-table tr:last-child td {
349
+ border-bottom: none;
350
+ }
351
+
352
+ .condition-name {
353
+ font-weight: 600;
354
+ color: var(--dark-color);
355
+ }
356
+
357
+ .classification-badge {
358
+ display: inline-flex;
359
+ align-items: center;
360
+ padding: 0.5rem 1rem;
361
+ border-radius: var(--radius-md);
362
+ font-weight: 600;
363
+ font-size: 1.1rem;
364
+ }
365
+
366
+ .confidence-display {
367
+ display: flex;
368
+ align-items: center;
369
+ gap: 1rem;
370
+ }
371
+
372
+ .progress-bar {
373
+ flex: 1;
374
+ height: 20px;
375
+ background: var(--border-color);
376
+ border-radius: 10px;
377
+ overflow: hidden;
378
+ position: relative;
379
+ }
380
+
381
+ .progress-fill {
382
+ height: 100%;
383
+ border-radius: 10px;
384
+ transition: width 1s ease-in-out;
385
+ position: relative;
386
+ overflow: hidden;
387
+ }
388
+
389
+ .progress-fill::after {
390
+ content: '';
391
+ position: absolute;
392
+ top: 0;
393
+ left: 0;
394
+ right: 0;
395
+ bottom: 0;
396
+ background: linear-gradient(90deg,
397
+ transparent 0%,
398
+ rgba(255, 255, 255, 0.3) 50%,
399
+ transparent 100%);
400
+ animation: shimmer 2s infinite;
401
+ }
402
+
403
+ @keyframes shimmer {
404
+ 0% { transform: translateX(-100%); }
405
+ 100% { transform: translateX(100%); }
406
+ }
407
+
408
+ .confidence-value {
409
+ font-weight: 700;
410
+ font-size: 1.2rem;
411
+ min-width: 70px;
412
+ text-align: right;
413
+ color: var(--dark-color);
414
+ }
415
+
416
+ /* Recommendations section */
417
+ .recommendations-box {
418
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
419
+ padding: 2rem;
420
+ border-radius: var(--radius-lg);
421
+ margin: 2rem 0;
422
+ border-left: 4px solid var(--info-color);
423
+ }
424
+
425
+ .recommendations-header {
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 0.75rem;
429
+ margin-bottom: 1.5rem;
430
+ color: var(--info-color);
431
+ }
432
+
433
+ .recommendations-header h3 {
434
+ font-size: 1.5rem;
435
+ font-weight: 600;
436
+ }
437
+
438
+ .recommendation-item {
439
+ margin-bottom: 1.5rem;
440
+ }
441
+
442
+ .recommendation-item:last-child {
443
+ margin-bottom: 0;
444
+ }
445
+
446
+ .recommendation-item h4 {
447
+ color: var(--dark-color);
448
+ font-size: 1.1rem;
449
+ margin-bottom: 0.5rem;
450
+ display: flex;
451
+ align-items: center;
452
+ gap: 0.5rem;
453
+ }
454
+
455
+ .recommendation-item p {
456
+ color: var(--gray-color);
457
+ line-height: 1.6;
458
+ margin: 0;
459
+ padding-left: 1.5rem;
460
+ }
461
+
462
+ /* Disclaimer */
463
+ .disclaimer-box {
464
+ background: linear-gradient(135deg, #fef3c7 0%, #fef9c3 100%);
465
+ padding: 1.5rem;
466
+ border-radius: var(--radius-lg);
467
+ border-left: 4px solid var(--warning-color);
468
+ margin-top: 2rem;
469
+ }
470
+
471
+ .disclaimer-box strong {
472
+ color: var(--warning-color);
473
+ display: block;
474
+ margin-bottom: 0.5rem;
475
+ font-size: 1rem;
476
+ }
477
+
478
+ .disclaimer-box p {
479
+ color: var(--dark-color);
480
+ font-size: 0.9rem;
481
+ line-height: 1.5;
482
+ margin: 0;
483
+ }
484
+
485
+ /* Examples section */
486
+ .examples-section {
487
+ background: white;
488
+ padding: 2rem;
489
+ border-radius: var(--radius-xl);
490
+ box-shadow: var(--shadow-md);
491
+ margin-top: 2rem;
492
+ border: 1px solid var(--border-color);
493
+ }
494
+
495
+ .examples-header {
496
+ display: flex;
497
+ align-items: center;
498
+ gap: 0.75rem;
499
+ margin-bottom: 1.5rem;
500
+ color: var(--primary-color);
501
+ }
502
+
503
+ .examples-header h3 {
504
+ font-size: 1.5rem;
505
+ font-weight: 600;
506
+ }
507
+
508
+ .example-images {
509
+ display: grid;
510
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
511
+ gap: 1rem;
512
+ }
513
+
514
+ .example-image {
515
+ border-radius: var(--radius-md);
516
+ overflow: hidden;
517
+ cursor: pointer;
518
+ transition: all 0.3s ease;
519
+ border: 2px solid transparent;
520
+ }
521
+
522
+ .example-image:hover {
523
+ transform: translateY(-4px);
524
+ box-shadow: var(--shadow-lg);
525
+ border-color: var(--primary-light);
526
+ }
527
+
528
+ /* API Section */
529
+ .api-section {
530
+ background: white;
531
+ padding: 2rem;
532
+ border-radius: var(--radius-xl);
533
+ box-shadow: var(--shadow-md);
534
+ margin-top: 2rem;
535
+ border: 1px solid var(--border-color);
536
+ }
537
+
538
+ .api-accordion summary {
539
+ display: flex;
540
+ align-items: center;
541
+ gap: 0.75rem;
542
+ cursor: pointer;
543
+ padding: 1rem 0;
544
+ color: var(--primary-color);
545
+ font-size: 1.25rem;
546
+ font-weight: 600;
547
+ border-bottom: 2px solid var(--border-color);
548
+ user-select: none;
549
+ }
550
+
551
+ .api-content {
552
+ padding: 1.5rem 0;
553
+ }
554
+
555
+ .api-endpoint {
556
+ background: var(--light-color);
557
+ padding: 1.5rem;
558
+ border-radius: var(--radius-lg);
559
+ margin-bottom: 1rem;
560
+ border-left: 4px solid var(--secondary-color);
561
+ }
562
+
563
+ .api-endpoint h4 {
564
+ color: var(--dark-color);
565
+ margin-bottom: 0.75rem;
566
+ font-size: 1.1rem;
567
+ }
568
+
569
+ .api-endpoint pre {
570
+ background: var(--dark-color);
571
+ color: white;
572
+ padding: 1rem;
573
+ border-radius: var(--radius-md);
574
+ overflow-x: auto;
575
+ margin: 0.75rem 0;
576
+ font-size: 0.9rem;
577
+ }
578
+
579
+ .api-endpoint code {
580
+ font-family: 'Monaco', 'Consolas', monospace;
581
+ }
582
+
583
+ /* Footer */
584
+ .footer {
585
+ text-align: center;
586
+ padding: 2rem 0;
587
+ color: var(--gray-color);
588
+ font-size: 0.9rem;
589
+ margin-top: 3rem;
590
+ border-top: 1px solid var(--border-color);
591
+ }
592
+
593
+ /* Responsive adjustments */
594
+ @media (max-width: 768px) {
595
+ .gradio-container {
596
+ padding: 1rem !important;
597
+ }
598
+
599
+ .header-title {
600
+ font-size: 2rem;
601
+ }
602
+
603
+ .header-subtitle {
604
+ font-size: 1rem;
605
+ }
606
+
607
+ .content-wrapper {
608
+ gap: 1rem;
609
+ }
610
+
611
+ .upload-container,
612
+ .results-container {
613
+ padding: 1.5rem;
614
+ }
615
+
616
+ .results-table th,
617
+ .results-table td {
618
+ padding: 1rem;
619
+ }
620
+ }
621
+
622
+ /* Dark mode support */
623
+ .dark .gradio-container {
624
+ background: #1a1a1a !important;
625
+ }
626
+
627
+ .dark .upload-container,
628
+ .dark .results-container,
629
+ .dark .examples-section,
630
+ .dark .api-section {
631
+ background: #2d2d2d !important;
632
+ border-color: #404040 !important;
633
+ color: #e5e5e5 !important;
634
+ }
635
+
636
+ .dark .upload-guide,
637
+ .dark .recommendations-box,
638
+ .dark .disclaimer-box {
639
+ background: #333333 !important;
640
+ }
641
+
642
+ .dark .results-table th {
643
+ background: #333333 !important;
644
+ color: #e5e5e5 !important;
645
+ }
646
+
647
+ .dark .api-endpoint pre {
648
+ background: #333333 !important;
649
+ color: #e5e5e5 !important;
650
+ }
651
+ """
652
+
653
  # ============================================================
654
  # 1. LOAD MODEL (with Hugging Face compatibility)
655
  # ============================================================
 
657
  print("🚀 LOADING MODEL FOR HUGGING FACE SPACES")
658
  print("=" * 60)
659
 
660
+ MODEL_PATHS = ["model.keras", "./model.keras", "/tmp/model.keras"]
 
 
 
 
 
661
 
662
  best_model = None
663
  for model_path in MODEL_PATHS:
664
  if os.path.exists(model_path):
665
  try:
666
+ print(f"📂 Loading model from: {model_path}")
667
+ best_model = tf.keras.models.load_model(model_path, compile=False, safe_mode=False)
668
+ print(f"✅ Model loaded successfully")
 
 
 
 
669
  break
670
  except Exception as e:
671
+ print(f"❌ Failed to load: {e}")
672
 
 
673
  if best_model is None:
674
+ print("⚠️ Creating dummy model for demo...")
675
  from tensorflow.keras import layers, Model
676
  inputs = layers.Input(shape=(224, 224, 3))
677
  x = layers.GlobalAveragePooling2D()(inputs)
678
  dr_output = layers.Dense(5, name="dr_head")(x)
679
  dme_output = layers.Dense(3, name="dme_head")(x)
680
  best_model = Model(inputs, {"dr_head": dr_output, "dme_head": dme_output})
 
681
  print("✅ Dummy model created")
682
 
 
 
 
 
 
 
683
  # ============================================================
684
  # 2. CONFIG
685
  # ============================================================
 
687
  DR_CLASSES = ["No DR", "Mild", "Moderate", "Severe", "Proliferative DR"]
688
  DME_CLASSES = ["No DME", "Low Risk", "High Risk"]
689
 
690
+ # Color mapping for each class
691
+ COLOR_MAP = {
692
+ "No DR": "#10b981", # Green
693
+ "Mild": "#f59e0b", # Yellow
694
+ "Moderate": "#f97316", # Orange
695
+ "Severe": "#ef4444", # Red
696
+ "Proliferative DR": "#8b5cf6", # Purple
697
+ "No DME": "#10b981", # Green
698
+ "Low Risk": "#f59e0b", # Yellow
699
+ "High Risk": "#ef4444" # Red
700
+ }
701
+
702
  # ============================================================
703
+ # 3. PREDICTION FUNCTIONS
704
  # ============================================================
705
  def preprocess_pil_image(img):
 
706
  if img.mode != 'RGB':
707
  img = img.convert('RGB')
708
  img = img.resize((IMG_SIZE, IMG_SIZE))
709
  arr = np.array(img, dtype=np.float32) / 255.0
710
  return np.expand_dims(arr, 0)
711
 
 
 
 
712
  def ensure_probability(x):
713
  x = np.asarray(x, dtype=np.float32)
714
  if x.min() < 0 or x.max() > 1.0 or abs(x.sum() - 1.0) > 1e-3:
715
  x = tf.nn.softmax(x).numpy()
716
  return x
717
 
 
 
 
718
  def predict_image(image):
 
719
  try:
 
720
  img_tensor = preprocess_pil_image(image)
 
 
721
  preds = best_model.predict(img_tensor, verbose=0)
722
 
 
723
  dr_pred = None
724
  dme_pred = None
725
 
 
753
  dr_pred = preds[:5]
754
  dme_pred = preds[5:8]
755
 
 
756
  if dr_pred is not None and len(dr_pred.shape) > 1:
757
  dr_pred = dr_pred[0]
758
  if dme_pred is not None and len(dme_pred.shape) > 1:
759
  dme_pred = dme_pred[0]
760
 
761
+ dr_pred = dr_pred if dr_pred is not None else np.zeros(5)
762
+ dme_pred = dme_pred if dme_pred is not None else np.zeros(3)
 
 
763
 
 
764
  dr_probs = ensure_probability(dr_pred)
765
  dme_probs = ensure_probability(dme_pred)
766
 
 
767
  dr_idx = int(np.argmax(dr_probs))
768
  dme_idx = int(np.argmax(dme_probs))
769
 
 
773
  dr_conf = float(dr_probs[dr_idx] * 100)
774
  dme_conf = float(dme_probs[dme_idx] * 100)
775
 
776
+ # Recommendations
777
+ recommendations = {
778
+ "No DR": "Lanjutkan pola hidup sehat dan lakukan pemeriksaan mata rutin minimal 1 tahun sekali.",
779
+ "Mild": "Disarankan kontrol gula darah secara ketat dan pemeriksaan mata berkala setiap 6 bulan.",
780
+ "Moderate": "Disarankan kontrol gula darah secara ketat dan pemeriksaan mata berkala setiap 6 bulan.",
781
+ "Severe": "Disarankan segera konsultasi ke dokter spesialis mata untuk evaluasi dan penanganan lebih lanjut.",
782
+ "Proliferative DR": "Disarankan segera konsultasi ke dokter spesialis mata untuk evaluasi dan penanganan lebih lanjut.",
783
+ "No DME": "Belum ditemukan tanda edema makula diabetik, lanjutkan pemantauan rutin.",
784
+ "Low Risk": "Perlu observasi ketat dan pemeriksaan lanjutan untuk mencegah progresivitas.",
785
+ "High Risk": "Disarankan segera mendapatkan evaluasi klinis dan terapi oleh dokter spesialis mata."
786
+ }
 
 
 
787
 
788
  return {
789
  "success": True,
790
+ "dr": {
791
+ "name": dr_name,
792
+ "confidence": dr_conf,
793
+ "color": COLOR_MAP.get(dr_name, "#6b7280"),
794
+ "recommendation": recommendations.get(dr_name, "")
795
+ },
796
+ "dme": {
797
+ "name": dme_name,
798
+ "confidence": dme_conf,
799
+ "color": COLOR_MAP.get(dme_name, "#6b7280"),
800
+ "recommendation": recommendations.get(dme_name, "")
 
 
 
 
801
  }
802
  }
803
 
804
  except Exception as e:
805
+ return {"success": False, "error": str(e)}
 
 
 
806
 
807
  # ============================================================
808
+ # 4. HTML FORMATTING FUNCTIONS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
  # ============================================================
810
  def format_prediction_html(result):
 
811
  if not result["success"]:
812
  return f"""
813
+ <div class="results-container">
814
+ <div class="results-header">
815
+ <span class="results-icon"></span>
816
+ <h3>Error</h3>
817
+ </div>
818
+ <div class="results-content">
819
+ <div style="color: #ef4444; padding: 1rem; background: #fef2f2; border-radius: 0.5rem;">
820
+ <strong>Error:</strong> {result['error']}
821
+ </div>
822
+ </div>
823
  </div>
824
  """
825
 
826
+ dr = result["dr"]
827
+ dme = result["dme"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
828
 
829
+ return f"""
830
+ <div class="results-content">
831
+ <div class="results-title">
832
+ <h2>🔬 HASIL DETEKSI</h2>
833
+ <p>AI-Powered Retina Analysis</p>
834
+ </div>
835
+
836
+ <table class="results-table">
 
 
837
  <thead>
838
+ <tr>
839
+ <th>Kondisi</th>
840
+ <th>Klasifikasi</th>
841
+ <th>Tingkat Kepercayaan</th>
842
  </tr>
843
  </thead>
844
  <tbody>
845
  <tr>
846
+ <td class="condition-name">Diabetic Retinopathy (DR)</td>
847
+ <td>
848
+ <span class="classification-badge" style="background: {dr['color']}20; color: {dr['color']};">
849
+ {dr['name']}
850
+ </span>
851
  </td>
852
+ <td>
853
+ <div class="confidence-display">
854
+ <div class="progress-bar">
855
+ <div class="progress-fill" style="width: {dr['confidence']}%; background: {dr['color']};"></div>
856
  </div>
857
+ <span class="confidence-value">{dr['confidence']:.1f}%</span>
858
  </div>
859
  </td>
860
  </tr>
861
  <tr>
862
+ <td class="condition-name">Diabetic Macular Edema (DME)</td>
863
+ <td>
864
+ <span class="classification-badge" style="background: {dme['color']}20; color: {dme['color']};">
865
+ {dme['name']}
866
+ </span>
867
  </td>
868
+ <td>
869
+ <div class="confidence-display">
870
+ <div class="progress-bar">
871
+ <div class="progress-fill" style="width: {dme['confidence']}%; background: {dme['color']};"></div>
872
  </div>
873
+ <span class="confidence-value">{dme['confidence']:.1f}%</span>
874
  </div>
875
  </td>
876
  </tr>
877
  </tbody>
878
  </table>
879
+
880
+ <div class="recommendations-box">
881
+ <div class="recommendations-header">
882
+ <span>🩺</span>
883
+ <h3>REKOMENDASI KLINIS</h3>
 
 
 
 
 
884
  </div>
885
+
886
+ <div class="recommendation-item">
887
+ <h4><span style="color: {dr['color']}">•</span> Diabetic Retinopathy (DR)</h4>
888
+ <p>{dr['recommendation']}</p>
889
+ </div>
890
+
891
+ <div class="recommendation-item">
892
+ <h4><span style="color: {dme['color']}">•</span> Diabetic Macular Edema (DME)</h4>
893
+ <p>{dme['recommendation']}</p>
894
  </div>
895
  </div>
896
+
897
+ <div class="disclaimer-box">
898
+ <strong>⚠️ Disclaimer Medis</strong>
899
+ <p>Hasil ini merupakan prediksi AI dan bukan diagnosis medis. Konsultasikan dengan dokter spesialis mata untuk diagnosis yang akurat dan penanganan lebih lanjut.</p>
900
+ </div>
901
  </div>
902
  """
 
903
 
904
  def gradio_predict(image):
 
905
  if image is None:
906
+ return """
907
+ <div class="results-placeholder">
908
+ <h4>👁️ Siap untuk Analisis</h4>
909
+ <p>Upload gambar retina untuk memulai deteksi AI</p>
910
+ <div class="arrow-indicator">⬅️</div>
911
+ </div>
912
+ """
913
 
914
  result = predict_image(image)
915
  return format_prediction_html(result)
916
 
917
  # ============================================================
918
+ # 5. CREATE GRADIO APP WITH CUSTOM CSS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
919
  # ============================================================
920
  with gr.Blocks(
921
+ css=CUSTOM_CSS,
922
+ theme=gr.themes.Soft(
923
+ primary_hue="indigo",
924
+ secondary_hue="gray",
925
+ font=("Inter", "ui-sans-serif", "system-ui", "sans-serif")
926
+ ),
927
+ title="DR & DME Detection System",
928
+ analytics_enabled=False
929
  ) as demo:
930
 
931
+ # Header Section
932
+ gr.HTML("""
933
+ <div class="header-section">
934
+ <div class="header-title">🩺 DETEKSI DIABETIC RETINOPATHY & DME</div>
935
+ <div class="header-subtitle">Sistem AI untuk Analisis Citra Fundus Retina</div>
936
+ <div class="header-description">
937
+ Deteksi dini kerusakan retina akibat diabetes dan pembengkakan di makula dengan teknologi AI.
938
+ Upload gambar fundus retina untuk mendapatkan analisis instan.
939
+ </div>
940
+ </div>
941
  """)
942
 
943
+ # Main Content
944
+ with gr.Row(elem_classes="content-wrapper"):
945
+ # Left Column - Upload
946
+ with gr.Column(scale=1, min_width=400):
947
+ gr.HTML("""
948
+ <div class="upload-container">
949
+ <div class="upload-header">
950
+ <span class="upload-icon">📤</span>
951
+ <h3>Upload Gambar Retina</h3>
952
+ </div>
953
+ <div class="upload-guide">
954
+ <h4>📋 Panduan Upload</h4>
955
+ <ul>
956
+ <li>Format: JPG, PNG, JPEG</li>
957
+ <li>Ukuran: 224×224 piksel (otomatis)</li>
958
+ <li>Warna: RGB (otomatis konversi)</li>
959
+ <li>Kualitas: Gambar jelas tanpa blur</li>
960
+ </ul>
961
+ </div>
962
+ </div>
963
+ """)
964
+
965
  image_input = gr.Image(
966
  type="pil",
967
+ label=" ",
968
+ height=320,
969
+ show_label=False,
970
+ elem_id="image-upload"
971
  )
972
 
973
+ predict_btn = gr.Button(
974
+ "🔍 Analisis Gambar dengan AI",
975
+ elem_classes="analyze-button",
976
+ scale=1
977
  )
978
 
979
+ gr.HTML("""
980
+ <div class="upload-guide">
981
+ <h4>💡 Tips Hasil Terbaik</h4>
982
+ <ul>
983
+ <li>Pastikan gambar fokus pada retina</li>
984
+ <li>Hindari cahaya berlebihan</li>
985
+ <li>Gunakan gambar dengan kontras baik</li>
986
+ <li>Pastikan seluruh area retina terlihat</li>
987
+ </ul>
988
+ </div>
989
  """)
990
 
991
+ # Right Column - Results
992
+ with gr.Column(scale=2, min_width=600):
993
+ gr.HTML("""
994
+ <div class="results-container">
995
+ <div class="results-header">
996
+ <span class="results-icon">📊</span>
997
+ <h3>Hasil Analisis</h3>
998
+ </div>
999
+ <div id="results-output">
1000
+ <div class="results-placeholder">
1001
+ <h4>👁️ Siap untuk Analisis</h4>
1002
+ <p>Upload gambar retina untuk memulai deteksi AI</p>
1003
+ <div class="arrow-indicator">⬅️</div>
1004
+ </div>
1005
+ </div>
1006
+ </div>
1007
+ """)
1008
+
1009
  output_html = gr.HTML(
1010
+ elem_id="results-output",
1011
+ visible=False
1012
  )
 
 
 
 
 
 
1013
 
1014
+ # Examples Section
1015
+ TEST_IMAGES = [[p] for p in [
1016
+ "IDRiD_001test.jpg", "IDRiD_004test.jpg", "IDRiD_005test.jpg",
1017
+ "IDRiD_006test.jpg", "IDRiD_007test.jpg", "IDRiD_008test.jpg"
1018
+ ] if os.path.exists(p)]
1019
+
1020
+ if TEST_IMAGES:
1021
+ with gr.Row():
1022
+ with gr.Column():
1023
+ gr.HTML("""
1024
+ <div class="examples-section">
1025
+ <div class="examples-header">
1026
+ <span>🧪</span>
1027
+ <h3>Contoh Gambar Testing</h3>
1028
+ </div>
1029
+ </div>
1030
+ """)
1031
+
1032
+ gr.Examples(
1033
+ examples=TEST_IMAGES,
1034
+ inputs=image_input,
1035
+ label="Klik untuk mencoba",
1036
+ examples_per_page=3
1037
+ )
1038
+
1039
+ # API Section
1040
+ with gr.Row():
1041
+ with gr.Column():
1042
+ with gr.Accordion("📱 Akses API untuk Mobile App", open=False, elem_classes="api-section"):
1043
+ gr.Markdown("""
1044
+ ## API Endpoints
1045
+
1046
+ ### Predict Endpoint
1047
+ **URL:** `POST https://kodetr-idrid.hf.space/run/predict`
1048
+
1049
+ **Content-Type:** `multipart/form-data`
1050
+
1051
+ **Parameter:** `data` (file gambar)
1052
+
1053
+ **Contoh cURL:**
1054
+ ```bash
1055
+ curl -X POST "https://kodetr-idrid.hf.space/run/predict" \\
1056
+ -F "data=@retina_image.jpg"
1057
+ ```
1058
+
1059
+ **Contoh Python:**
1060
+ ```python
1061
+ import requests
1062
+
1063
+ with open("retina.jpg", "rb") as f:
1064
+ response = requests.post(
1065
+ "https://kodetr-idrid.hf.space/run/predict",
1066
+ files={{"data": f}}
1067
+ )
1068
+ print(response.json())
1069
+ ```
1070
+
1071
+ ### Response Format:
1072
+ ```json
1073
+ {{
1074
+ "success": true,
1075
+ "dr": {{
1076
+ "name": "No DR",
1077
+ "confidence": 85.5,
1078
+ "recommendation": "..."
1079
+ }},
1080
+ "dme": {{
1081
+ "name": "No DME",
1082
+ "confidence": 92.3,
1083
+ "recommendation": "..."
1084
+ }}
1085
+ }}
1086
+ ```
1087
+ """)
1088
+
1089
+ # Footer
1090
+ gr.HTML("""
1091
+ <div class="footer">
1092
+ <p>🩺 DR & DME Detection System v1.0 • AI-Powered Medical Analysis</p>
1093
+ <p style="font-size: 0.8rem; opacity: 0.7; margin-top: 0.5rem;">
1094
+ Disclaimer: Sistem ini untuk tujuan edukasi dan penelitian. Selalu konsultasikan dengan dokter spesialis.
1095
+ </p>
1096
+ </div>
1097
+ """)
1098
+
1099
+ # Connect components
1100
+ def update_results(image):
1101
+ if image is None:
1102
+ return gr.HTML("""
1103
+ <div class="results-placeholder">
1104
+ <h4>👁️ Siap untuk Analisis</h4>
1105
+ <p>Upload gambar retina untuk memulai deteksi AI</p>
1106
+ <div class="arrow-indicator">⬅️</div>
1107
+ </div>
1108
+ """, visible=True), gr.HTML(visible=False)
1109
+ else:
1110
+ result = predict_image(image)
1111
+ html_output = format_prediction_html(result)
1112
+ return gr.HTML(visible=False), gr.HTML(html_output, visible=True)
1113
 
1114
+ predict_btn.click(
1115
+ fn=update_results,
1116
  inputs=image_input,
1117
+ outputs=[output_html, output_html]
1118
+ )
1119
+
1120
+ image_input.change(
1121
+ fn=update_results,
1122
+ inputs=image_input,
1123
+ outputs=[output_html, output_html]
1124
  )
1125
 
1126
  # ============================================================
1127
+ # 6. CREATE FASTAPI APP (optional, for API endpoints)
1128
  # ============================================================
1129
+ app = FastAPI(title="DR & DME Detection API")
 
 
 
 
 
 
 
 
 
 
 
 
1130
 
1131
+ app.add_middleware(
1132
+ CORSMiddleware,
1133
+ allow_origins=["*"],
1134
+ allow_credentials=True,
1135
+ allow_methods=["*"],
1136
+ allow_headers=["*"],
1137
+ )
1138
 
1139
  @app.post("/api/predict")
1140
+ async def api_predict(file: UploadFile = File(...)):
 
 
 
 
1141
  try:
1142
  if not file.content_type.startswith('image/'):
1143
  raise HTTPException(status_code=400, detail="File must be an image")
 
1152
 
1153
  return JSONResponse(content=result)
1154
 
 
 
1155
  except Exception as e:
1156
  raise HTTPException(status_code=500, detail=str(e))
1157
 
1158
+ # Mount Gradio app
 
 
 
1159
  app = gr.mount_gradio_app(app, demo, path="/")
1160
 
1161
  # ============================================================
1162
+ # 7. MAIN ENTRY POINT
1163
  # ============================================================
1164
  if __name__ == "__main__":
1165
  print("\n" + "="*60)
1166
+ print("🚀 DR & DME Detection System Starting...")
1167
  print("="*60)
1168
+ print(f"🖥️ Web Interface: http://localhost:7860")
1169
+ print(f"📱 API Endpoint: http://localhost:7860/api/predict")
1170
+ print(f"🔗 Gradio API: http://localhost:7860/run/predict")
 
 
1171
  print("="*60)
1172
 
1173
+ demo.launch(
1174
+ server_name="0.0.0.0",
1175
+ server_port=7860,
1176
+ share=False,
1177
+ debug=False
1178
  )