AbhinavGupta commited on
Commit
4d8dc65
Β·
verified Β·
1 Parent(s): 08bb5bd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +290 -231
app.py CHANGED
@@ -1,25 +1,6 @@
1
- """
2
- =================================================================
3
- DISASTER AI β€” HuggingFace Spaces API (Permanent Free Server)
4
  =================================================================
5
- Deploy this to HuggingFace Spaces for a permanent, always-on
6
- API that your friends can call without knowing any AI.
7
-
8
- Setup:
9
- 1. Go to https://huggingface.co/spaces
10
- 2. Create New Space β†’ SDK: Docker (or Gradio)
11
- 3. Repo name: EgoisticCoderX/dokai-inference-api
12
- 4. Upload this file as app.py
13
- 5. Upload requirements.txt
14
- 6. Add secrets in Space Settings:
15
- - HF_VICTIM_MODEL_REPO = EgoisticCoderX/dokai-victim-detection
16
- - ROBOFLOW_API_KEY = your_key
17
-
18
- Your API will be at:
19
- https://egoisticcoderx-dokai-inference-api.hf.space/
20
-
21
- ZeroGPU Note: HF Spaces has ZeroGPU (A10G, free) for inference.
22
- Add @spaces.GPU decorator for GPU-accelerated endpoints.
23
  =================================================================
24
  """
25
 
@@ -29,6 +10,7 @@ import json
29
  import time
30
  import base64
31
  import threading
 
32
  import numpy as np
33
  from pathlib import Path
34
  from PIL import Image
@@ -36,28 +18,18 @@ import cv2
36
  import torch
37
  import requests
38
 
39
- from fastapi import FastAPI, File, UploadFile, HTTPException, Depends
40
  from fastapi.middleware.cors import CORSMiddleware
41
  from fastapi.responses import JSONResponse
42
  from huggingface_hub import hf_hub_download
43
 
44
- # ── ZeroGPU support (HuggingFace Spaces) ──
45
- try:
46
- import spaces
47
- HAS_ZERO_GPU = True
48
- print("βœ… ZeroGPU available")
49
- except ImportError:
50
- HAS_ZERO_GPU = False
51
- print("ℹ️ ZeroGPU not available (running locally or non-GPU space)")
52
-
53
-
54
  # ════════════════════════════════
55
  # App Setup
56
  # ════════════════════════════════
57
  app = FastAPI(
58
  title="Disaster AI Inference API",
59
- description="Multi-model disaster scene analysis API for Dokai/RoboXavier",
60
- version="1.0.0",
61
  )
62
 
63
  app.add_middleware(
@@ -70,7 +42,7 @@ app.add_middleware(
70
  # ════════════════════════════════
71
  # Configuration
72
  # ════════════════════════════════
73
- HF_VICTIM_MODEL_REPO = os.getenv("HF_VICTIM_MODEL_REPO", "EgoisticCoderX/dokai-victim-detection")
74
  ROBOFLOW_API_KEY = os.getenv("ROBOFLOW_API_KEY", "")
75
  MODEL_CACHE_DIR = "/tmp/model_cache"
76
  os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
@@ -89,84 +61,137 @@ CLASS_PRIORITY = {
89
  "rescue_personnel": 0.0,
90
  }
91
 
92
-
93
  # ════════════════════════════════
94
- # Model Registry β€” lazy loading
95
  # ════════════════════════════════
96
  class ModelRegistry:
97
- """
98
- Lazy-loads models on first request.
99
- Prevents OOM on Space startup.
100
- """
101
  def __init__(self):
102
- self._models = {}
103
- self._lock = threading.Lock()
104
- self._loading = set()
105
 
106
- def get(self, name: str):
107
- if name in self._models:
108
- return self._models[name]
109
- return None
110
 
111
- def register(self, name: str, model):
112
  with self._lock:
113
  self._models[name] = model
114
  print(f"βœ… Model registered: {name}")
115
 
116
- def is_loaded(self, name: str) -> bool:
117
- return name in self._models
118
-
 
119
 
120
- registry = ModelRegistry()
 
121
 
 
 
122
 
123
- def load_victim_model():
124
- """Download + load YOLOv8 victim detection model from HuggingFace."""
125
- if registry.is_loaded("victim"):
126
- return registry.get("victim")
 
127
 
128
- try:
129
- from ultralytics import YOLO
130
- model_path = hf_hub_download(
131
- repo_id=HF_VICTIM_MODEL_REPO,
132
- filename="best.pt",
133
- cache_dir=MODEL_CACHE_DIR,
134
- )
135
- model = YOLO(model_path)
136
- registry.register("victim", model)
137
- return model
138
- except Exception as e:
139
- print(f"❌ Failed to load victim model: {e}")
140
- return None
141
 
 
 
 
142
 
143
  def load_ladi_model():
144
- """Load LADI-v2 classifier from HuggingFace."""
145
  if registry.is_loaded("ladi"):
146
  return registry.get("ladi")
147
 
148
  try:
149
  from transformers import AutoImageProcessor, AutoModelForImageClassification
 
 
 
150
  processor = AutoImageProcessor.from_pretrained(
151
  "MITLL/LADI-v2-classifier-small",
152
  cache_dir=MODEL_CACHE_DIR,
153
  )
 
154
  model = AutoModelForImageClassification.from_pretrained(
155
  "MITLL/LADI-v2-classifier-small",
156
  cache_dir=MODEL_CACHE_DIR,
 
 
157
  )
158
  model.eval()
 
 
159
  registry.register("ladi", {"model": model, "processor": processor})
 
160
  return registry.get("ladi")
 
161
  except Exception as e:
162
- print(f"❌ Failed to load LADI model: {e}")
 
163
  return None
164
 
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  # ════════════════════════════════
167
- # Utility Functions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  # ════════════════════════════════
169
- def read_image_from_upload(file_bytes: bytes) -> np.ndarray:
170
  nparr = np.frombuffer(file_bytes, np.uint8)
171
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
172
  if img is None:
@@ -175,7 +200,6 @@ def read_image_from_upload(file_bytes: bytes) -> np.ndarray:
175
 
176
 
177
  def call_roboflow(image: np.ndarray, model_id: str, confidence: int = 40) -> list:
178
- """Call Roboflow hosted model (ambulance, vest detection)."""
179
  if not ROBOFLOW_API_KEY:
180
  return []
181
  try:
@@ -192,12 +216,12 @@ def call_roboflow(image: np.ndarray, model_id: str, confidence: int = 40) -> lis
192
  preds = res.json().get("predictions", [])
193
  return [
194
  {
195
- "class": p["class"],
196
  "confidence": round(p["confidence"], 4),
197
  "box": {
198
- "xmin": int(p["x"] - p["width"] / 2),
199
  "ymin": int(p["y"] - p["height"] / 2),
200
- "xmax": int(p["x"] + p["width"] / 2),
201
  "ymax": int(p["y"] + p["height"] / 2),
202
  },
203
  }
@@ -209,31 +233,28 @@ def call_roboflow(image: np.ndarray, model_id: str, confidence: int = 40) -> lis
209
 
210
 
211
  def compute_triage(detections: list) -> dict:
212
- """Compute triage priority summary from victim detections."""
213
  if not detections:
214
  return {
215
- "total": 0,
216
- "critical": 0,
217
- "high": 0,
218
- "moderate": 0,
219
- "low": 0,
220
  "highest_score": 0.0,
221
  "action": "βœ… No victims detected",
 
222
  }
223
 
224
  scored = []
225
  for d in detections:
226
  cls_name = d.get("class", "")
227
- conf = d.get("confidence", 0.5)
228
- weight = CLASS_PRIORITY.get(cls_name, 0.5)
229
- score = conf * weight
230
- rank = (
231
- "CRITICAL" if score >= 0.7
232
- else "HIGH" if score >= 0.4
233
- else "MODERATE" if score >= 0.2
234
- else "LOW"
235
  )
236
- scored.append({**d, "priority_score": round(score, 4), "priority_rank": rank})
237
 
238
  scored.sort(key=lambda x: x["priority_score"], reverse=True)
239
 
@@ -243,10 +264,10 @@ def compute_triage(detections: list) -> dict:
243
  low = sum(1 for d in scored if d["priority_rank"] == "LOW")
244
 
245
  action = (
246
- "⚠️ IMMEDIATE RESCUE β€” Critical victims present" if critical
247
- else "πŸ”΄ Deploy rescue team β€” High priority victims" if high
248
- else "🟑 Assess and triage β€” Moderate victims present" if moderate
249
- else "🟒 Low priority β€” Monitor the area"
250
  )
251
 
252
  return {
@@ -268,15 +289,15 @@ def compute_triage(detections: list) -> dict:
268
  @app.get("/")
269
  def root():
270
  return {
271
- "service": "Disaster AI Inference API",
272
- "version": "1.0.0",
 
273
  "endpoints": {
274
- "/health": "GET β€” Service health check",
275
- "/detect/victims": "POST β€” Victim detection + triage",
276
- "/detect/hazards": "POST β€” Fire, smoke, building damage",
277
- "/detect/vehicles": "POST β€” Emergency vehicle detection",
278
- "/classify": "POST β€” LADI-v2 scene classification",
279
- "/analyze/full": "POST β€” All models in parallel (full analysis)",
280
  }
281
  }
282
 
@@ -284,73 +305,155 @@ def root():
284
  @app.get("/health")
285
  def health():
286
  return {
287
- "status": "ok",
288
- "models_loaded": {
289
- "victim_detection": registry.is_loaded("victim"),
290
- "ladi_classifier": registry.is_loaded("ladi"),
291
- },
292
  "gpu_available": torch.cuda.is_available(),
293
- "timestamp": time.time(),
294
  }
295
 
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  @app.post("/detect/victims")
298
  async def detect_victims(
299
- file: UploadFile = File(...),
300
  confidence: float = 0.35,
301
  ):
302
  """
303
  Detect victims and classify by triage priority.
304
- Returns detections with priority scores and recommended action.
305
  """
306
- contents = await file.read()
307
- img = read_image_from_upload(contents)
308
-
309
  model = load_victim_model()
310
  if model is None:
311
- raise HTTPException(status_code=503, detail="Victim detection model unavailable")
 
 
 
 
 
 
312
 
313
  t0 = time.time()
314
- results = model.predict(source=img, conf=confidence, verbose=False)
 
 
 
315
  elapsed = round((time.time() - t0) * 1000, 2)
316
 
317
- raw_detections = []
318
  for r in results:
319
  for box in r.boxes:
320
  x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
321
  conf_val = float(box.conf[0])
322
  cls_id = int(box.cls[0])
323
- raw_detections.append({
324
  "class": TARGET_CLASSES.get(cls_id, "unknown"),
325
  "class_id": cls_id,
326
  "confidence": round(conf_val, 4),
327
  "box": {"xmin": x1, "ymin": y1, "xmax": x2, "ymax": y2},
328
  })
329
 
330
- triage = compute_triage(raw_detections)
 
331
 
332
  return {
333
- "detections": triage.pop("ranked_victims", raw_detections),
334
- "triage_summary": triage,
335
  "inference_time_ms": elapsed,
336
  }
337
 
338
 
 
 
 
339
  @app.post("/detect/vehicles")
340
  async def detect_vehicles(file: UploadFile = File(...)):
341
- """Detect emergency vehicles using Roboflow model."""
 
 
 
 
 
 
342
  contents = await file.read()
343
- img = read_image_from_upload(contents)
344
 
345
- t0 = time.time()
346
  detections = call_roboflow(img, "ambulance-4bova/1", confidence=40)
347
- elapsed = round((time.time() - t0) * 1000, 2)
348
 
349
- has_ambulance = any("ambulance" in d["class"].lower() for d in detections)
350
- has_fire_truck = any("fire" in d["class"].lower() for d in detections)
351
 
352
  return {
353
- "detections": detections,
354
  "emergency_vehicles": {
355
  "ambulance_detected": has_ambulance,
356
  "fire_truck_detected": has_fire_truck,
@@ -360,128 +463,84 @@ async def detect_vehicles(file: UploadFile = File(...)):
360
  }
361
 
362
 
363
- @app.post("/classify")
364
- async def classify_scene(file: UploadFile = File(...), top_k: int = 5):
365
- """Classify disaster scene using LADI-v2."""
366
- contents = await file.read()
367
- img_pil = Image.open(io.BytesIO(contents)).convert("RGB")
368
-
369
- ladi = load_ladi_model()
370
- if ladi is None:
371
- raise HTTPException(status_code=503, detail="LADI-v2 model unavailable")
372
-
373
- model = ladi["model"]
374
- processor = ladi["processor"]
375
-
376
- t0 = time.time()
377
- inputs = processor(images=img_pil, return_tensors="pt")
378
- with torch.no_grad():
379
- outputs = model(**inputs)
380
- probs = torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
381
- elapsed = round((time.time() - t0) * 1000, 2)
382
-
383
- id2label = model.config.id2label
384
- all_scores = sorted(
385
- [
386
- {
387
- "class": id2label[i].lower().replace(" ", "_"),
388
- "confidence": round(float(probs[i]), 4),
389
- }
390
- for i in range(len(probs))
391
- ],
392
- key=lambda x: x["confidence"],
393
- reverse=True,
394
- )
395
-
396
- return {
397
- "top_predictions": all_scores[:top_k],
398
- "all_scores": all_scores,
399
- "inference_time_ms": elapsed,
400
- }
401
-
402
-
403
  @app.post("/analyze/full")
404
  async def full_analysis(
405
- file: UploadFile = File(...),
406
  run_victims: bool = True,
407
  run_vehicles: bool = True,
408
  run_classify: bool = True,
409
  ):
410
  """
411
- Run all available models in parallel on one image.
412
- This is what your rover Flask app should call for full scene analysis.
413
 
414
- Returns a unified JSON with all detections + zone scoring.
415
  """
416
- import asyncio
417
- from concurrent.futures import ThreadPoolExecutor
418
-
419
  contents = await file.read()
420
- t_total = time.time()
421
-
422
- results = {}
423
-
424
- with ThreadPoolExecutor(max_workers=3) as executor:
425
- futures = {}
426
-
427
- if run_victims:
428
- async def _victims():
429
- f = UploadFile(filename="frame.jpg", file=io.BytesIO(contents))
430
- return await detect_victims(f)
431
- futures["victims"] = asyncio.ensure_future(_victims())
432
-
433
- if run_vehicles:
434
- async def _vehicles():
435
- f = UploadFile(filename="frame.jpg", file=io.BytesIO(contents))
436
- return await detect_vehicles(f)
437
- futures["vehicles"] = asyncio.ensure_future(_vehicles())
438
-
439
- if run_classify:
440
- async def _classify():
441
- f = UploadFile(filename="frame.jpg", file=io.BytesIO(contents))
442
- return await classify_scene(f)
443
- futures["classification"] = asyncio.ensure_future(_classify())
444
-
445
- for key, fut in futures.items():
446
- try:
447
- results[key] = await fut
448
- except Exception as e:
449
- results[key] = {"error": str(e)}
450
-
451
- total_ms = round((time.time() - t_total) * 1000, 2)
452
-
453
- # Determine overall zone color
454
- victim_data = results.get("victims", {})
455
- triage = victim_data.get("triage_summary", {})
456
- classify_data = results.get("classification", {})
457
-
458
- critical = triage.get("critical", 0)
459
- high = triage.get("high", 0)
460
-
461
- top_class = ""
462
- if classify_data.get("top_predictions"):
463
- top_class = classify_data["top_predictions"][0].get("class", "")
464
-
465
- if critical > 0 or "destroy" in top_class or "collapse" in top_class:
466
  zone_color = "red"
467
- elif high > 0 or "major_damage" in top_class:
468
  zone_color = "orange"
469
- elif triage.get("total", 0) > 0:
470
  zone_color = "yellow"
471
  else:
472
  zone_color = "green"
473
 
474
  return {
475
- "zone_color": zone_color,
476
- "results": results,
477
- "total_time_ms": total_ms,
478
- "timestamp": time.time(),
479
  }
480
 
481
 
482
  # ════════════════════════════════
483
- # Entry Point (local testing)
484
  # ════════════════════════════════
485
  if __name__ == "__main__":
486
  import uvicorn
487
- uvicorn.run(app, host="0.0.0.0", port=7860) # HF Spaces default port
 
 
 
 
1
  =================================================================
2
+ Disaster AI β€” HuggingFace Spaces API
3
+ Final version β€” all fixes applied
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  =================================================================
5
  """
6
 
 
10
  import time
11
  import base64
12
  import threading
13
+ import traceback
14
  import numpy as np
15
  from pathlib import Path
16
  from PIL import Image
 
18
  import torch
19
  import requests
20
 
21
+ from fastapi import FastAPI, File, UploadFile, HTTPException
22
  from fastapi.middleware.cors import CORSMiddleware
23
  from fastapi.responses import JSONResponse
24
  from huggingface_hub import hf_hub_download
25
 
 
 
 
 
 
 
 
 
 
 
26
  # ════════════════════════════════
27
  # App Setup
28
  # ════════════════════════════════
29
  app = FastAPI(
30
  title="Disaster AI Inference API",
31
+ description="Multi-model disaster scene analysis for Dokai / RoboXavier",
32
+ version="2.0.0",
33
  )
34
 
35
  app.add_middleware(
 
42
  # ════════════════════════════════
43
  # Configuration
44
  # ════════════════════════════════
45
+ HF_VICTIM_MODEL_REPO = os.getenv("HF_VICTIM_MODEL_REPO", "")
46
  ROBOFLOW_API_KEY = os.getenv("ROBOFLOW_API_KEY", "")
47
  MODEL_CACHE_DIR = "/tmp/model_cache"
48
  os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
 
61
  "rescue_personnel": 0.0,
62
  }
63
 
 
64
  # ════════════════════════════════
65
+ # Model Registry
66
  # ════════════════════════════════
67
  class ModelRegistry:
 
 
 
 
68
  def __init__(self):
69
+ self._models = {}
70
+ self._errors = {}
71
+ self._lock = threading.Lock()
72
 
73
+ def get(self, name):
74
+ return self._models.get(name)
 
 
75
 
76
+ def register(self, name, model):
77
  with self._lock:
78
  self._models[name] = model
79
  print(f"βœ… Model registered: {name}")
80
 
81
+ def set_error(self, name, error):
82
+ with self._lock:
83
+ self._errors[name] = str(error)
84
+ print(f"❌ Model error [{name}]: {error}")
85
 
86
+ def is_loaded(self, name):
87
+ return name in self._models
88
 
89
+ def get_error(self, name):
90
+ return self._errors.get(name, "Unknown error")
91
 
92
+ def status(self):
93
+ return {
94
+ "loaded": list(self._models.keys()),
95
+ "errored": {k: v for k, v in self._errors.items()},
96
+ }
97
 
98
+ registry = ModelRegistry()
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ # ════════════════════════════════
101
+ # Model Loaders
102
+ # ════════════════════════════════
103
 
104
  def load_ladi_model():
105
+ """Load LADI-v2 classifier from HuggingFace Hub."""
106
  if registry.is_loaded("ladi"):
107
  return registry.get("ladi")
108
 
109
  try:
110
  from transformers import AutoImageProcessor, AutoModelForImageClassification
111
+
112
+ print("⬇️ Loading MITLL/LADI-v2-classifier-small ...")
113
+
114
  processor = AutoImageProcessor.from_pretrained(
115
  "MITLL/LADI-v2-classifier-small",
116
  cache_dir=MODEL_CACHE_DIR,
117
  )
118
+
119
  model = AutoModelForImageClassification.from_pretrained(
120
  "MITLL/LADI-v2-classifier-small",
121
  cache_dir=MODEL_CACHE_DIR,
122
+ trust_remote_code=True,
123
+ ignore_mismatched_sizes=True,
124
  )
125
  model.eval()
126
+
127
+ # CPU only on HF free tier
128
  registry.register("ladi", {"model": model, "processor": processor})
129
+ print("βœ… LADI-v2 ready")
130
  return registry.get("ladi")
131
+
132
  except Exception as e:
133
+ print(f"❌ LADI-v2 load failed:\n{traceback.format_exc()}")
134
+ registry.set_error("ladi", e)
135
  return None
136
 
137
 
138
+ def load_victim_model():
139
+ """Load YOLOv8 victim detection model from HuggingFace Hub."""
140
+ if registry.is_loaded("victim"):
141
+ return registry.get("victim")
142
+
143
+ if not HF_VICTIM_MODEL_REPO:
144
+ registry.set_error("victim", "HF_VICTIM_MODEL_REPO secret not set β€” train the model first")
145
+ return None
146
+
147
+ try:
148
+ from ultralytics import YOLO
149
+
150
+ print(f"⬇️ Loading victim model from {HF_VICTIM_MODEL_REPO} ...")
151
+ model_path = hf_hub_download(
152
+ repo_id=HF_VICTIM_MODEL_REPO,
153
+ filename="best.pt",
154
+ cache_dir=MODEL_CACHE_DIR,
155
+ )
156
+ model = YOLO(model_path)
157
+ registry.register("victim", model)
158
+ print("βœ… Victim detection model ready")
159
+ return model
160
+
161
+ except Exception as e:
162
+ print(f"❌ Victim model load failed:\n{traceback.format_exc()}")
163
+ registry.set_error("victim", e)
164
+ return None
165
+
166
+
167
+ # ════════════════════════════════
168
+ # Startup β€” preload everything
169
  # ════════════════════════════════
170
+ @app.on_event("startup")
171
+ async def startup_event():
172
+ print("\n" + "="*50)
173
+ print("πŸš€ Disaster AI API starting up...")
174
+ print("="*50)
175
+
176
+ # Always load LADI β€” it's a public HF model
177
+ load_ladi_model()
178
+
179
+ # Only load victim model if repo is configured
180
+ if HF_VICTIM_MODEL_REPO:
181
+ load_victim_model()
182
+ else:
183
+ print("⚠️ Victim model skipped β€” HF_VICTIM_MODEL_REPO not set")
184
+ print(" Train the model first, then add the secret to this Space")
185
+
186
+ print("="*50)
187
+ print(f"πŸ“Š Registry status: {registry.status()}")
188
+ print("="*50 + "\n")
189
+
190
+
191
+ # ════════════════════════════════
192
+ # Utility
193
  # ════════════════════════════════
194
+ def read_image(file_bytes: bytes) -> np.ndarray:
195
  nparr = np.frombuffer(file_bytes, np.uint8)
196
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
197
  if img is None:
 
200
 
201
 
202
  def call_roboflow(image: np.ndarray, model_id: str, confidence: int = 40) -> list:
 
203
  if not ROBOFLOW_API_KEY:
204
  return []
205
  try:
 
216
  preds = res.json().get("predictions", [])
217
  return [
218
  {
219
+ "class": p["class"],
220
  "confidence": round(p["confidence"], 4),
221
  "box": {
222
+ "xmin": int(p["x"] - p["width"] / 2),
223
  "ymin": int(p["y"] - p["height"] / 2),
224
+ "xmax": int(p["x"] + p["width"] / 2),
225
  "ymax": int(p["y"] + p["height"] / 2),
226
  },
227
  }
 
233
 
234
 
235
  def compute_triage(detections: list) -> dict:
 
236
  if not detections:
237
  return {
238
+ "total": 0, "critical": 0, "high": 0,
239
+ "moderate": 0, "low": 0,
 
 
 
240
  "highest_score": 0.0,
241
  "action": "βœ… No victims detected",
242
+ "ranked_victims": [],
243
  }
244
 
245
  scored = []
246
  for d in detections:
247
  cls_name = d.get("class", "")
248
+ conf = d.get("confidence", 0.5)
249
+ weight = CLASS_PRIORITY.get(cls_name, 0.5)
250
+ score = round(conf * weight, 4)
251
+ rank = (
252
+ "CRITICAL" if score >= 0.7 else
253
+ "HIGH" if score >= 0.4 else
254
+ "MODERATE" if score >= 0.2 else
255
+ "LOW"
256
  )
257
+ scored.append({**d, "priority_score": score, "priority_rank": rank})
258
 
259
  scored.sort(key=lambda x: x["priority_score"], reverse=True)
260
 
 
264
  low = sum(1 for d in scored if d["priority_rank"] == "LOW")
265
 
266
  action = (
267
+ "⚠️ IMMEDIATE RESCUE β€” Critical victims present" if critical else
268
+ "πŸ”΄ Deploy rescue team β€” High priority victims" if high else
269
+ "🟑 Assess and triage β€” Moderate victims present" if moderate else
270
+ "🟒 Low priority β€” Monitor the area"
271
  )
272
 
273
  return {
 
289
  @app.get("/")
290
  def root():
291
  return {
292
+ "service": "Disaster AI Inference API",
293
+ "version": "2.0.0",
294
+ "status": registry.status(),
295
  "endpoints": {
296
+ "GET /health": "Health check + model status",
297
+ "POST /classify": "LADI-v2 scene classification",
298
+ "POST /detect/victims": "Victim detection + triage priority",
299
+ "POST /detect/vehicles": "Emergency vehicle detection",
300
+ "POST /analyze/full": "All models in one call",
 
301
  }
302
  }
303
 
 
305
  @app.get("/health")
306
  def health():
307
  return {
308
+ "status": "ok",
309
+ "registry": registry.status(),
 
 
 
310
  "gpu_available": torch.cuda.is_available(),
311
+ "timestamp": time.time(),
312
  }
313
 
314
 
315
+ # ─────────────────────────────────────────────
316
+ # LADI-v2 Classification
317
+ # ─────────────────────────────────────────────
318
+ @app.post("/classify")
319
+ async def classify_scene(
320
+ file: UploadFile = File(...),
321
+ top_k: int = 5,
322
+ ):
323
+ """
324
+ Classify disaster scene using LADI-v2.
325
+ Returns top-k predicted damage categories with confidence scores.
326
+ """
327
+ ladi = load_ladi_model()
328
+ if ladi is None:
329
+ raise HTTPException(
330
+ status_code=503,
331
+ detail=f"LADI-v2 unavailable: {registry.get_error('ladi')}"
332
+ )
333
+
334
+ contents = await file.read()
335
+ try:
336
+ img_pil = Image.open(io.BytesIO(contents)).convert("RGB")
337
+ except Exception:
338
+ raise HTTPException(status_code=400, detail="Invalid image")
339
+
340
+ model = ladi["model"]
341
+ processor = ladi["processor"]
342
+
343
+ t0 = time.time()
344
+ try:
345
+ inputs = processor(images=img_pil, return_tensors="pt")
346
+ with torch.no_grad():
347
+ outputs = model(**inputs)
348
+ probs = torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
349
+ except Exception as e:
350
+ raise HTTPException(status_code=500, detail=f"Inference failed: {e}")
351
+
352
+ elapsed = round((time.time() - t0) * 1000, 2)
353
+
354
+ id2label = model.config.id2label
355
+ all_scores = sorted(
356
+ [
357
+ {
358
+ "class": id2label[i].lower().replace(" ", "_"),
359
+ "confidence": round(float(probs[i]), 4),
360
+ }
361
+ for i in range(len(probs))
362
+ ],
363
+ key=lambda x: x["confidence"],
364
+ reverse=True,
365
+ )
366
+
367
+ # Exclude water/flood from top predictions (not relevant for rover)
368
+ relevant = [
369
+ s for s in all_scores
370
+ if not any(w in s["class"] for w in ["water", "flood"])
371
+ ]
372
+
373
+ return {
374
+ "top_predictions": all_scores[:top_k],
375
+ "relevant_only": relevant[:top_k],
376
+ "all_scores": all_scores,
377
+ "inference_time_ms": elapsed,
378
+ }
379
+
380
+
381
+ # ─────────────────────────────────────────────
382
+ # Victim Detection
383
+ # ─────────────────────────────────────────────
384
  @app.post("/detect/victims")
385
  async def detect_victims(
386
+ file: UploadFile = File(...),
387
  confidence: float = 0.35,
388
  ):
389
  """
390
  Detect victims and classify by triage priority.
391
+ Returns CRITICAL / HIGH / MODERATE / LOW ranked detections.
392
  """
 
 
 
393
  model = load_victim_model()
394
  if model is None:
395
+ raise HTTPException(
396
+ status_code=503,
397
+ detail=f"Victim model unavailable: {registry.get_error('victim')}"
398
+ )
399
+
400
+ contents = await file.read()
401
+ img = read_image(contents)
402
 
403
  t0 = time.time()
404
+ try:
405
+ results = model.predict(source=img, conf=confidence, verbose=False)
406
+ except Exception as e:
407
+ raise HTTPException(status_code=500, detail=f"Inference failed: {e}")
408
  elapsed = round((time.time() - t0) * 1000, 2)
409
 
410
+ raw = []
411
  for r in results:
412
  for box in r.boxes:
413
  x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
414
  conf_val = float(box.conf[0])
415
  cls_id = int(box.cls[0])
416
+ raw.append({
417
  "class": TARGET_CLASSES.get(cls_id, "unknown"),
418
  "class_id": cls_id,
419
  "confidence": round(conf_val, 4),
420
  "box": {"xmin": x1, "ymin": y1, "xmax": x2, "ymax": y2},
421
  })
422
 
423
+ triage = compute_triage(raw)
424
+ victims = triage.pop("ranked_victims", raw)
425
 
426
  return {
427
+ "detections": victims,
428
+ "triage_summary": triage,
429
  "inference_time_ms": elapsed,
430
  }
431
 
432
 
433
+ # ─────────────────────────────────────────────
434
+ # Emergency Vehicle Detection
435
+ # ─────────────────────────────────────────────
436
  @app.post("/detect/vehicles")
437
  async def detect_vehicles(file: UploadFile = File(...)):
438
+ """
439
+ Detect emergency vehicles using Roboflow.
440
+ Returns ambulance / fire truck / rescue vehicle detections.
441
+ """
442
+ if not ROBOFLOW_API_KEY:
443
+ raise HTTPException(status_code=503, detail="ROBOFLOW_API_KEY secret not set")
444
+
445
  contents = await file.read()
446
+ img = read_image(contents)
447
 
448
+ t0 = time.time()
449
  detections = call_roboflow(img, "ambulance-4bova/1", confidence=40)
450
+ elapsed = round((time.time() - t0) * 1000, 2)
451
 
452
+ has_ambulance = any("ambulance" in d["class"].lower() for d in detections)
453
+ has_fire_truck = any("fire" in d["class"].lower() for d in detections)
454
 
455
  return {
456
+ "detections": detections,
457
  "emergency_vehicles": {
458
  "ambulance_detected": has_ambulance,
459
  "fire_truck_detected": has_fire_truck,
 
463
  }
464
 
465
 
466
+ # ─────────────────────────────────────────────
467
+ # Full Analysis β€” all models in one call
468
+ # ─────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  @app.post("/analyze/full")
470
  async def full_analysis(
471
+ file: UploadFile = File(...),
472
  run_victims: bool = True,
473
  run_vehicles: bool = True,
474
  run_classify: bool = True,
475
  ):
476
  """
477
+ Run all available models on one image.
478
+ This is the main endpoint your rover Flask app should call.
479
 
480
+ Returns unified JSON with zone color, all detections, triage summary.
481
  """
 
 
 
482
  contents = await file.read()
483
+ t_total = time.time()
484
+ output = {}
485
+
486
+ # ── LADI classification ──
487
+ if run_classify:
488
+ try:
489
+ fake_file = UploadFile(filename="f.jpg", file=io.BytesIO(contents))
490
+ output["classification"] = await classify_scene(fake_file)
491
+ except HTTPException as e:
492
+ output["classification"] = {"error": e.detail}
493
+ except Exception as e:
494
+ output["classification"] = {"error": str(e)}
495
+
496
+ # ── Victim detection ──
497
+ if run_victims:
498
+ try:
499
+ fake_file = UploadFile(filename="f.jpg", file=io.BytesIO(contents))
500
+ output["victims"] = await detect_victims(fake_file)
501
+ except HTTPException as e:
502
+ output["victims"] = {"error": e.detail}
503
+ except Exception as e:
504
+ output["victims"] = {"error": str(e)}
505
+
506
+ # ── Vehicle detection ──
507
+ if run_vehicles:
508
+ try:
509
+ fake_file = UploadFile(filename="f.jpg", file=io.BytesIO(contents))
510
+ output["vehicles"] = await detect_vehicles(fake_file)
511
+ except HTTPException as e:
512
+ output["vehicles"] = {"error": e.detail}
513
+ except Exception as e:
514
+ output["vehicles"] = {"error": str(e)}
515
+
516
+ # ── Zone color ──
517
+ triage_data = output.get("victims", {}).get("triage_summary", {})
518
+ classify_top = output.get("classification", {}).get("top_predictions", [{}])
519
+ top_class = classify_top[0].get("class", "") if classify_top else ""
520
+
521
+ critical = triage_data.get("critical", 0)
522
+ high = triage_data.get("high", 0)
523
+
524
+ if critical > 0 or any(w in top_class for w in ["destroy", "collapse", "major"]):
 
 
 
 
525
  zone_color = "red"
526
+ elif high > 0 or "minor_damage" in top_class:
527
  zone_color = "orange"
528
+ elif triage_data.get("total", 0) > 0:
529
  zone_color = "yellow"
530
  else:
531
  zone_color = "green"
532
 
533
  return {
534
+ "zone_color": zone_color,
535
+ "results": output,
536
+ "total_time_ms": round((time.time() - t_total) * 1000, 2),
537
+ "timestamp": time.time(),
538
  }
539
 
540
 
541
  # ════════════════════════════════
542
+ # Entry Point
543
  # ════════════════════════════════
544
  if __name__ == "__main__":
545
  import uvicorn
546
+ uvicorn.run(app, host="0.0.0.0", port=7860)