muhammadpriv001 commited on
Commit
d7f2e92
Β·
1 Parent(s): 3b12341

fix: use ASGI middleware to intercept /api/* before Gradio catch-all route

Browse files
Files changed (1) hide show
  1. app.py +85 -96
app.py CHANGED
@@ -11,6 +11,8 @@ import gradio as gr
11
  from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request
12
  from fastapi.responses import JSONResponse, FileResponse
13
  from fastapi.middleware.cors import CORSMiddleware
 
 
14
 
15
  # ---------------------------------------------------------------------------
16
  # Hugging Face ZeroGPU startup probe
@@ -206,22 +208,9 @@ def teach_new_object(name, category, description, reference_images):
206
 
207
 
208
  # ---------------------------------------------------------------------------
209
- # API helper functions (non-GPU, used inside Gradio's FastAPI)
210
  # ---------------------------------------------------------------------------
211
 
212
- def _run_api_detection(img_bgr, mode, lock_mode, lock_target, prompts, conf_val, sim_val):
213
- annotated_bgr, detections, metadata = orchestrator.process_frame(
214
- image=img_bgr,
215
- mode=mode,
216
- lock_mode=lock_mode,
217
- lock_target=lock_target,
218
- open_vocab_prompts=prompts,
219
- confidence_thresh=conf_val,
220
- similarity_thresh=sim_val
221
- )
222
- return annotated_bgr, detections, metadata
223
-
224
-
225
  def _handle_api_health():
226
  return {
227
  "status": "healthy",
@@ -251,14 +240,14 @@ def _handle_api_detect(file_bytes, mode, lock_mode, lock_target, open_vocab_prom
251
  raw_vocab = open_vocab_prompts or vocabulary
252
  prompts = [p.strip() for p in raw_vocab.split(",") if p.strip()] if raw_vocab else None
253
 
254
- annotated_frame, detections, metadata = _run_api_detection(
255
- img_bgr=img,
256
  mode=mode,
257
  lock_mode=is_lock_mode,
258
  lock_target=target_val,
259
- prompts=prompts,
260
- conf_val=conf_val,
261
- sim_val=SPECIFIC_SIMILARITY_THRESHOLD
262
  )
263
 
264
  _, buffer = cv2.imencode(".png", annotated_frame)
@@ -307,6 +296,80 @@ def _handle_api_export_db():
307
  return zip_path
308
 
309
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  # ---------------------------------------------------------------------------
311
  # Building Modern Glassmorphic Gradio UI
312
  # ---------------------------------------------------------------------------
@@ -374,9 +437,6 @@ with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform")
374
 
375
  with gr.Tabs():
376
 
377
- # -------------------------------------------------------------------
378
- # TAB 1: Live Testing & Lock Mode
379
- # -------------------------------------------------------------------
380
  with gr.TabItem("πŸ‘οΈ Live Testing & Lock Mode"):
381
  with gr.Row():
382
  with gr.Column(scale=1):
@@ -431,9 +491,6 @@ with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform")
431
  api_name="run_detection"
432
  )
433
 
434
- # -------------------------------------------------------------------
435
- # TAB 2: Object Studio (Teach & Manage)
436
- # -------------------------------------------------------------------
437
  with gr.TabItem("πŸŽ“ Object Studio (Teach & Manage)"):
438
  gr.Markdown("## Teach Specific Objects to the System")
439
  gr.Markdown("Upload reference photos of your personal physical object (e.g. *My Cup*, *Lab Keyring*). The system extracts L2 normalized visual embeddings to recognize this exact object identity.")
@@ -470,9 +527,6 @@ with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform")
470
  outputs=[teach_status, objects_df]
471
  )
472
 
473
- # -------------------------------------------------------------------
474
- # TAB 3: Model & Database Downloads
475
- # -------------------------------------------------------------------
476
  with gr.TabItem("πŸ’Ύ Downloads & Package Export"):
477
  gr.Markdown("## Downloads, Models & Custom Object Database Export")
478
  gr.Markdown("Download pretrained models, database export packages, SDK packages, and verified checksum manifests.")
@@ -494,9 +548,6 @@ with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform")
494
  | **ResNet18 Backbone** | Feature Embedding Extractor | `7d88e2...03` | Torchvision |
495
  """)
496
 
497
- # -------------------------------------------------------------------
498
- # TAB 4: Developer Guides & Documentation
499
- # -------------------------------------------------------------------
500
  with gr.TabItem("πŸ“š Developer Guides"):
501
  with gr.Row():
502
  doc_selector = gr.Dropdown(
@@ -517,9 +568,6 @@ with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform")
517
 
518
  doc_selector.change(fn=load_doc_file, inputs=[doc_selector], outputs=[doc_markdown_view])
519
 
520
- # -------------------------------------------------------------------
521
- # TAB 5: REST API Reference
522
- # -------------------------------------------------------------------
523
  with gr.TabItem("πŸ”Œ REST API"):
524
  gr.Markdown("## REST API & FastAPI Endpoint Specifications")
525
  gr.Markdown("""
@@ -541,7 +589,7 @@ with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform")
541
 
542
 
543
  # ---------------------------------------------------------------------------
544
- # Monkey-patch Gradio's create_app to inject our FastAPI routes
545
  # ---------------------------------------------------------------------------
546
  from gradio.routes import App as _GradioApp
547
 
@@ -559,71 +607,12 @@ def _patched_create_app(blocks, *args, **kwargs):
559
  allow_headers=["*"],
560
  )
561
 
 
 
562
  @fastapi_app.on_event("startup")
563
  async def _startup():
564
  _zerogpu_startup_probe()
565
 
566
- @fastapi_app.get("/api/health")
567
- async def api_health():
568
- return _handle_api_health()
569
-
570
- @fastapi_app.post("/api/detect")
571
- async def api_detect_route(
572
- file: UploadFile = File(...),
573
- mode: str = Form("combined"),
574
- lock_mode: str = Form("false"),
575
- lock_target: str = Form(""),
576
- open_vocab_prompts: str = Form(""),
577
- vocabulary: str = Form(""),
578
- confidence: str = Form(""),
579
- ):
580
- contents = await file.read()
581
- if not contents:
582
- raise HTTPException(status_code=400, detail="Uploaded file is empty")
583
- try:
584
- return JSONResponse(content=_handle_api_detect(contents, mode, lock_mode, lock_target, open_vocab_prompts, vocabulary, confidence))
585
- except ValueError as e:
586
- raise HTTPException(status_code=400, detail=str(e))
587
-
588
- @fastapi_app.get("/api/objects")
589
- async def api_get_objects():
590
- return _handle_api_objects()
591
-
592
- @fastapi_app.post("/api/objects")
593
- async def api_teach_object(
594
- name: str = Form(...),
595
- category: str = Form("custom"),
596
- description: str = Form(""),
597
- files: list[UploadFile] = File(default=[]),
598
- ):
599
- try:
600
- files_bytes = []
601
- for uf in files:
602
- content = await uf.read()
603
- files_bytes.append((content, uf.filename))
604
- return JSONResponse(content=_handle_api_teach(name, category, description, files_bytes))
605
- except ValueError as e:
606
- raise HTTPException(status_code=400, detail=str(e))
607
-
608
- @fastapi_app.delete("/api/objects/{object_id}")
609
- async def api_delete_object(object_id: str):
610
- try:
611
- return JSONResponse(content=_handle_api_delete_object(object_id))
612
- except ValueError as e:
613
- raise HTTPException(status_code=404, detail=str(e))
614
-
615
- @fastapi_app.get("/api/downloads/export-db")
616
- async def api_export_db():
617
- zip_path = _handle_api_export_db()
618
- return FileResponse(zip_path, filename="custom_objects_library.zip", media_type="application/zip")
619
-
620
- @fastapi_app.get("/api/downloads/requirements")
621
- async def api_download_requirements():
622
- req_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")
623
- if os.path.exists(req_path):
624
- return FileResponse(req_path, filename="requirements.txt", media_type="text/plain")
625
- raise HTTPException(status_code=404, detail="requirements.txt not found")
626
-
627
  return fastapi_app
628
 
629
 
 
11
  from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request
12
  from fastapi.responses import JSONResponse, FileResponse
13
  from fastapi.middleware.cors import CORSMiddleware
14
+ from starlette.middleware.base import BaseHTTPMiddleware
15
+ from starlette.responses import Response
16
 
17
  # ---------------------------------------------------------------------------
18
  # Hugging Face ZeroGPU startup probe
 
208
 
209
 
210
  # ---------------------------------------------------------------------------
211
+ # API handler functions (non-GPU)
212
  # ---------------------------------------------------------------------------
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  def _handle_api_health():
215
  return {
216
  "status": "healthy",
 
240
  raw_vocab = open_vocab_prompts or vocabulary
241
  prompts = [p.strip() for p in raw_vocab.split(",") if p.strip()] if raw_vocab else None
242
 
243
+ annotated_frame, detections, metadata = orchestrator.process_frame(
244
+ image=img,
245
  mode=mode,
246
  lock_mode=is_lock_mode,
247
  lock_target=target_val,
248
+ open_vocab_prompts=prompts,
249
+ confidence_thresh=conf_val,
250
+ similarity_thresh=SPECIFIC_SIMILARITY_THRESHOLD
251
  )
252
 
253
  _, buffer = cv2.imencode(".png", annotated_frame)
 
296
  return zip_path
297
 
298
 
299
+ # ---------------------------------------------------------------------------
300
+ # ASGI Middleware β€” intercepts /api/* routes BEFORE Gradio's catch-all
301
+ # ---------------------------------------------------------------------------
302
+
303
+ class CustomAPIMiddleware(BaseHTTPMiddleware):
304
+ async def dispatch(self, request: Request, call_next):
305
+ path = request.url.path
306
+ method = request.method
307
+
308
+ if not path.startswith("/api/"):
309
+ return await call_next(request)
310
+
311
+ try:
312
+ if method == "GET" and path == "/api/health":
313
+ return JSONResponse(content=_handle_api_health())
314
+
315
+ elif method == "POST" and path == "/api/detect":
316
+ form = await request.form()
317
+ file = form.get("file")
318
+ if not file:
319
+ return JSONResponse(status_code=400, content={"detail": "Missing 'file' field"})
320
+ file_bytes = await file.read()
321
+ result = _handle_api_detect(
322
+ file_bytes,
323
+ form.get("mode", "combined"),
324
+ form.get("lock_mode", "false"),
325
+ form.get("lock_target", ""),
326
+ form.get("open_vocab_prompts", ""),
327
+ form.get("vocabulary", ""),
328
+ form.get("confidence", ""),
329
+ )
330
+ return JSONResponse(content=result)
331
+
332
+ elif method == "GET" and path == "/api/objects":
333
+ return JSONResponse(content=_handle_api_objects())
334
+
335
+ elif method == "POST" and path == "/api/objects":
336
+ form = await request.form()
337
+ name = form.get("name", "")
338
+ category = form.get("category", "custom")
339
+ description = form.get("description", "")
340
+ files_bytes = []
341
+ for key in form:
342
+ if key == "files" or key.startswith("files"):
343
+ upload_file = form[key]
344
+ content = await upload_file.read()
345
+ files_bytes.append((content, getattr(upload_file, "filename", "image")))
346
+ result = _handle_api_teach(name, category, description, files_bytes)
347
+ return JSONResponse(content=result)
348
+
349
+ elif method == "DELETE" and path.startswith("/api/objects/"):
350
+ object_id = path.split("/api/objects/")[-1]
351
+ try:
352
+ result = _handle_api_delete_object(object_id)
353
+ return JSONResponse(content=result)
354
+ except ValueError as e:
355
+ return JSONResponse(status_code=404, content={"detail": str(e)})
356
+
357
+ elif method == "GET" and path == "/api/downloads/export-db":
358
+ zip_path = _handle_api_export_db()
359
+ return FileResponse(zip_path, filename="custom_objects_library.zip", media_type="application/zip")
360
+
361
+ elif method == "GET" and path == "/api/downloads/requirements":
362
+ req_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")
363
+ if os.path.exists(req_path):
364
+ return FileResponse(req_path, filename="requirements.txt", media_type="text/plain")
365
+ return JSONResponse(status_code=404, content={"detail": "requirements.txt not found"})
366
+
367
+ except Exception as e:
368
+ return JSONResponse(status_code=500, content={"detail": str(e)})
369
+
370
+ return await call_next(request)
371
+
372
+
373
  # ---------------------------------------------------------------------------
374
  # Building Modern Glassmorphic Gradio UI
375
  # ---------------------------------------------------------------------------
 
437
 
438
  with gr.Tabs():
439
 
 
 
 
440
  with gr.TabItem("πŸ‘οΈ Live Testing & Lock Mode"):
441
  with gr.Row():
442
  with gr.Column(scale=1):
 
491
  api_name="run_detection"
492
  )
493
 
 
 
 
494
  with gr.TabItem("πŸŽ“ Object Studio (Teach & Manage)"):
495
  gr.Markdown("## Teach Specific Objects to the System")
496
  gr.Markdown("Upload reference photos of your personal physical object (e.g. *My Cup*, *Lab Keyring*). The system extracts L2 normalized visual embeddings to recognize this exact object identity.")
 
527
  outputs=[teach_status, objects_df]
528
  )
529
 
 
 
 
530
  with gr.TabItem("πŸ’Ύ Downloads & Package Export"):
531
  gr.Markdown("## Downloads, Models & Custom Object Database Export")
532
  gr.Markdown("Download pretrained models, database export packages, SDK packages, and verified checksum manifests.")
 
548
  | **ResNet18 Backbone** | Feature Embedding Extractor | `7d88e2...03` | Torchvision |
549
  """)
550
 
 
 
 
551
  with gr.TabItem("πŸ“š Developer Guides"):
552
  with gr.Row():
553
  doc_selector = gr.Dropdown(
 
568
 
569
  doc_selector.change(fn=load_doc_file, inputs=[doc_selector], outputs=[doc_markdown_view])
570
 
 
 
 
571
  with gr.TabItem("πŸ”Œ REST API"):
572
  gr.Markdown("## REST API & FastAPI Endpoint Specifications")
573
  gr.Markdown("""
 
589
 
590
 
591
  # ---------------------------------------------------------------------------
592
+ # Monkey-patch Gradio's create_app to inject our ASGI middleware
593
  # ---------------------------------------------------------------------------
594
  from gradio.routes import App as _GradioApp
595
 
 
607
  allow_headers=["*"],
608
  )
609
 
610
+ fastapi_app.add_middleware(CustomAPIMiddleware)
611
+
612
  @fastapi_app.on_event("startup")
613
  async def _startup():
614
  _zerogpu_startup_probe()
615
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
  return fastapi_app
617
 
618