muhammadpriv001 commited on
Commit
ffa910f
·
1 Parent(s): f65aafa

fix: rewrite app.py - standalone FastAPI with Gradio mounted, fix 422 and missing _run_api_detection

Browse files
Files changed (1) hide show
  1. app.py +137 -174
app.py CHANGED
@@ -5,28 +5,21 @@ import zipfile
5
  import cv2
6
  import numpy as np
7
  import spaces
 
 
 
 
 
8
 
9
  # ---------------------------------------------------------------------------
10
  # Hugging Face ZeroGPU startup probe
11
  # ---------------------------------------------------------------------------
12
- # ZeroGPU statically scans the module at startup and requires a module-level
13
- # @spaces.GPU function. Keep this tiny probe explicit so the Space is detected
14
- # even when the real GPU handlers are wrapped/imported differently.
15
  @spaces.GPU
16
  def _zerogpu_startup_probe():
17
  return None
18
 
19
- from PIL import Image
20
- import gradio as gr
21
- from fastapi import FastAPI, File, UploadFile, Form, HTTPException
22
- from fastapi.responses import JSONResponse, FileResponse
23
- from fastapi.middleware.cors import CORSMiddleware
24
-
25
  # ---------------------------------------------------------------------------
26
- # Monkey-patch gradio_client bug (Gradio 4.44.0):
27
- # gradio_client.utils._json_schema_to_python_type crashes with
28
- # TypeError: argument of type 'bool' is not iterable
29
- # when a JSON schema has `additionalProperties: true` (a bool, not a dict).
30
  # ---------------------------------------------------------------------------
31
  import gradio_client.utils as _gc_utils
32
 
@@ -42,10 +35,6 @@ _gc_utils._json_schema_to_python_type = _safe_json_schema_to_python_type
42
  # ---------------------------------------------------------------------------
43
  # Starlette / Gradio TemplateResponse & Jinja2 compatibility
44
  # ---------------------------------------------------------------------------
45
- # Gradio 4.44.0 calls TemplateResponse("index.html", {"request": request, ...})
46
- # Newer Starlette releases expect request as the first argument.
47
- # When mixed, Starlette misinterprets string vs dict argument order, passing
48
- # context dict to Jinja2 as template name -> TypeError: unhashable type: 'dict'.
49
  import jinja2
50
 
51
  _orig_get_template = jinja2.Environment.get_template
@@ -95,7 +84,6 @@ from backend.ml.orchestrator import orchestrator
95
  from backend.database.storage import db
96
 
97
 
98
-
99
  # ---------------------------------------------------------------------------
100
  # Helper functions (non-GPU)
101
  # ---------------------------------------------------------------------------
@@ -337,7 +325,7 @@ with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform")
337
  similarity_slider
338
  ],
339
  outputs=[output_img, json_output],
340
- api_name="run_detection"
341
  )
342
 
343
  # -------------------------------------------------------------------
@@ -370,7 +358,7 @@ with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform")
370
  fn=teach_new_object,
371
  inputs=[teach_name, teach_category, teach_desc, teach_files],
372
  outputs=[teach_status, objects_df],
373
- api_name="teach_object_api"
374
  )
375
 
376
  btn_del.click(
@@ -448,168 +436,143 @@ with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform")
448
  ```
449
  """)
450
 
 
451
  # ---------------------------------------------------------------------------
452
- # Launch — use demo.launch() so HF ZeroGPU can detect @spaces.GPU functions
453
  # ---------------------------------------------------------------------------
454
- demo.queue()
 
 
 
455
 
456
- from gradio.routes import App as _GradioApp
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
 
458
- _original_create = _GradioApp.create_app
 
 
 
459
 
460
- @staticmethod
461
- def _patched_create(blocks, **kwargs):
462
- fastapi_app = _original_create(blocks, **kwargs)
463
 
464
- fastapi_app.add_middleware(
465
- CORSMiddleware,
466
- allow_origins=["*"],
467
- allow_credentials=True,
468
- allow_methods=["*"],
469
- allow_headers=["*"],
 
 
 
 
 
 
 
 
 
470
  )
471
 
472
- from fastapi import Request
473
- from fastapi.exceptions import RequestValidationError
474
- from fastapi.responses import JSONResponse
475
-
476
- @fastapi_app.exception_handler(RequestValidationError)
477
- async def safe_validation_exception_handler(request: Request, exc: RequestValidationError):
478
- safe_errors = []
479
- for err in exc.errors():
480
- err_dict = dict(err)
481
- if "input" in err_dict and isinstance(err_dict["input"], bytes):
482
- err_dict["input"] = f"<binary input: {len(err_dict['input'])} bytes>"
483
- safe_errors.append(err_dict)
484
- return JSONResponse(status_code=422, content={"detail": safe_errors})
485
-
486
- @fastapi_app.get("/api/health")
487
- def api_health():
488
- return {
489
- "status": "healthy",
490
- "models": {
491
- "rtdetr": orchestrator.rtdetr.model is not None,
492
- "yolo_world": orchestrator.yolo_world.model is not None,
493
- "embedding_recognizer": orchestrator.embedding_recognizer.model is not None,
494
- },
495
- "database": os.path.exists(SQLITE_DB_PATH),
496
- }
497
-
498
- from typing import Optional
499
-
500
- @fastapi_app.post("/api/detect")
501
- async def api_detect_route(
502
- file: Optional[UploadFile] = File(None),
503
- mode: Optional[str] = Form(None),
504
- lock_mode: Optional[str] = Form(None),
505
- lock_target: Optional[str] = Form(None),
506
- open_vocab_prompts: Optional[str] = Form(None),
507
- vocabulary: Optional[str] = Form(None),
508
- confidence: Optional[str] = Form(None),
509
- ):
510
- try:
511
- if file is None:
512
- raise HTTPException(status_code=400, detail="Missing required 'file' parameter in form data")
513
-
514
- contents = await file.read()
515
- if not contents:
516
- raise HTTPException(status_code=400, detail="Uploaded file is empty")
517
-
518
- nparr = np.frombuffer(contents, np.uint8)
519
- img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
520
- if img is None:
521
- raise HTTPException(status_code=400, detail="Invalid image file format")
522
-
523
- mode_val = mode or "combined"
524
- target_val = lock_target or ""
525
- is_lock_mode = str(lock_mode or "false").strip().lower() in ("true", "1", "yes")
526
-
527
- try:
528
- conf_val = float(confidence) if confidence is not None else KNOWN_CONFIDENCE_THRESHOLD
529
- except Exception:
530
- conf_val = KNOWN_CONFIDENCE_THRESHOLD
531
-
532
- raw_vocab = open_vocab_prompts or vocabulary
533
- prompts = [p.strip() for p in raw_vocab.split(",") if p.strip()] if raw_vocab else None
534
- annotated_frame, detections, metadata = _run_api_detection(
535
- img, mode_val, is_lock_mode, target_val, prompts, conf_val
536
- )
537
- import base64
538
- _, buffer = cv2.imencode(".png", annotated_frame)
539
- img_base64 = base64.b64encode(buffer).decode("utf-8")
540
- return JSONResponse(content={"status": "success", "image_base64": img_base64, "metadata": metadata, "detections": detections})
541
- except HTTPException:
542
- raise
543
- except Exception as e:
544
- raise HTTPException(status_code=500, detail=str(e))
545
-
546
- @fastapi_app.get("/api/objects")
547
- def api_get_objects_route():
548
- objs = db.get_all_objects()
549
- return [{"id": o.id, "name": o.name, "category": o.category, "images": o.image_count, "image_count": o.image_count} for o in objs]
550
-
551
- @fastapi_app.post("/api/objects")
552
- async def api_teach_object_route(
553
- name: Optional[str] = Form(None),
554
- category: Optional[str] = Form(None),
555
- description: Optional[str] = Form(None),
556
- files: Optional[list[UploadFile]] = File(None),
557
- ):
558
- try:
559
- if not name or not name.strip():
560
- raise HTTPException(status_code=400, detail="Missing required 'name' parameter")
561
-
562
- cat_val = (category or "custom").strip() or "general"
563
- desc_val = (description or "").strip()
564
-
565
- existing = db.get_object_by_name(name.strip())
566
- obj = existing if existing else db.create_object(name=name.strip(), category=cat_val, description=desc_val)
567
- added = 0
568
- if files:
569
- for uf in files:
570
- contents = await uf.read()
571
- nparr = np.frombuffer(contents, np.uint8)
572
- img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
573
- if img is None:
574
- continue
575
- emb = orchestrator.embedding_recognizer.extract_embedding_from_image(img)
576
- if emb is not None:
577
- db.add_embedding(obj.id, emb.tolist())
578
- db.add_image_record(obj.id, uf.filename or "image")
579
- added += 1
580
- return {"status": "success", "message": f"Taught object '{obj.name}' with {added} embeddings", "id": obj.id}
581
- except HTTPException:
582
- raise
583
- except Exception as e:
584
- raise HTTPException(status_code=500, detail=str(e))
585
-
586
- @fastapi_app.delete("/api/objects/{object_id}")
587
- def api_delete_object_route(object_id: str):
588
- if db.delete_object(object_id):
589
- return {"status": "deleted"}
590
- raise HTTPException(status_code=404, detail="Object not found")
591
-
592
- @fastapi_app.get("/api/downloads/export-db")
593
- def api_export_db_route():
594
- temp_dir = tempfile.mkdtemp()
595
- zip_path = os.path.join(temp_dir, "custom_objects_library.zip")
596
- db.export_database_zip(zip_path)
597
- return FileResponse(zip_path, filename="custom_objects_library.zip", media_type="application/zip")
598
-
599
- @fastapi_app.get("/api/downloads/requirements")
600
- def api_download_requirements_route():
601
- req_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")
602
- if os.path.exists(req_path):
603
- return FileResponse(req_path, filename="requirements.txt", media_type="text/plain")
604
- raise HTTPException(status_code=404, detail="requirements.txt not found")
605
-
606
- return fastapi_app
607
-
608
- _GradioApp.create_app = _patched_create
609
 
610
  if __name__ == "__main__":
 
611
  port = int(os.environ.get("PORT", os.environ.get("GRADIO_SERVER_PORT", "7860")))
612
- demo.launch(server_name="0.0.0.0", server_port=port)
613
- else:
614
- port = int(os.environ.get("PORT", os.environ.get("GRADIO_SERVER_PORT", "7860")))
615
- demo.launch(server_name="0.0.0.0", server_port=port)
 
5
  import cv2
6
  import numpy as np
7
  import spaces
8
+ from PIL import Image
9
+ import gradio as gr
10
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request
11
+ from fastapi.responses import JSONResponse, FileResponse
12
+ from fastapi.middleware.cors import CORSMiddleware
13
 
14
  # ---------------------------------------------------------------------------
15
  # Hugging Face ZeroGPU startup probe
16
  # ---------------------------------------------------------------------------
 
 
 
17
  @spaces.GPU
18
  def _zerogpu_startup_probe():
19
  return None
20
 
 
 
 
 
 
 
21
  # ---------------------------------------------------------------------------
22
+ # Monkey-patch gradio_client bug (Gradio 4.44.0)
 
 
 
23
  # ---------------------------------------------------------------------------
24
  import gradio_client.utils as _gc_utils
25
 
 
35
  # ---------------------------------------------------------------------------
36
  # Starlette / Gradio TemplateResponse & Jinja2 compatibility
37
  # ---------------------------------------------------------------------------
 
 
 
 
38
  import jinja2
39
 
40
  _orig_get_template = jinja2.Environment.get_template
 
84
  from backend.database.storage import db
85
 
86
 
 
87
  # ---------------------------------------------------------------------------
88
  # Helper functions (non-GPU)
89
  # ---------------------------------------------------------------------------
 
325
  similarity_slider
326
  ],
327
  outputs=[output_img, json_output],
328
+ api_name="detect"
329
  )
330
 
331
  # -------------------------------------------------------------------
 
358
  fn=teach_new_object,
359
  inputs=[teach_name, teach_category, teach_desc, teach_files],
360
  outputs=[teach_status, objects_df],
361
+ api_name="teach_object"
362
  )
363
 
364
  btn_del.click(
 
436
  ```
437
  """)
438
 
439
+
440
  # ---------------------------------------------------------------------------
441
+ # Build FastAPI app and mount Gradio on it
442
  # ---------------------------------------------------------------------------
443
+ from typing import Optional
444
+ import base64
445
+
446
+ app = FastAPI(title="Object Intelligence Platform API")
447
 
448
+ app.add_middleware(
449
+ CORSMiddleware,
450
+ allow_origins=["*"],
451
+ allow_credentials=True,
452
+ allow_methods=["*"],
453
+ allow_headers=["*"],
454
+ )
455
+
456
+ @app.get("/api/health")
457
+ def api_health():
458
+ return {
459
+ "status": "healthy",
460
+ "models": {
461
+ "rtdetr": orchestrator.rtdetr.model is not None,
462
+ "yolo_world": orchestrator.yolo_world.model is not None,
463
+ "embedding_recognizer": orchestrator.embedding_recognizer.model is not None,
464
+ },
465
+ "database": os.path.exists(SQLITE_DB_PATH),
466
+ }
467
+
468
+
469
+ @app.post("/api/detect")
470
+ async def api_detect_route(
471
+ file: UploadFile = File(...),
472
+ mode: str = Form("combined"),
473
+ lock_mode: str = Form("false"),
474
+ lock_target: str = Form(""),
475
+ open_vocab_prompts: str = Form(""),
476
+ vocabulary: str = Form(""),
477
+ confidence: str = Form(""),
478
+ ):
479
+ contents = await file.read()
480
+ if not contents:
481
+ raise HTTPException(status_code=400, detail="Uploaded file is empty")
482
 
483
+ nparr = np.frombuffer(contents, np.uint8)
484
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
485
+ if img is None:
486
+ raise HTTPException(status_code=400, detail="Invalid image file format")
487
 
488
+ is_lock_mode = str(lock_mode).strip().lower() in ("true", "1", "yes")
489
+ target_val = lock_target or ""
 
490
 
491
+ try:
492
+ conf_val = float(confidence) if confidence else KNOWN_CONFIDENCE_THRESHOLD
493
+ except Exception:
494
+ conf_val = KNOWN_CONFIDENCE_THRESHOLD
495
+
496
+ raw_vocab = open_vocab_prompts or vocabulary
497
+ prompts = [p.strip() for p in raw_vocab.split(",") if p.strip()] if raw_vocab else None
498
+
499
+ annotated_frame, detections, metadata = orchestrator.process_frame(
500
+ image=img,
501
+ mode=mode,
502
+ lock_mode=is_lock_mode,
503
+ lock_target=target_val,
504
+ open_vocab_prompts=prompts,
505
+ confidence_thresh=conf_val
506
  )
507
 
508
+ _, buffer = cv2.imencode(".png", annotated_frame)
509
+ img_base64 = base64.b64encode(buffer).decode("utf-8")
510
+ return JSONResponse(content={"status": "success", "image_base64": img_base64, "metadata": metadata, "detections": detections})
511
+
512
+
513
+ @app.get("/api/objects")
514
+ def api_get_objects_route():
515
+ objs = db.get_all_objects()
516
+ return [{"id": o.id, "name": o.name, "category": o.category, "images": o.image_count, "image_count": o.image_count} for o in objs]
517
+
518
+
519
+ @app.post("/api/objects")
520
+ async def api_teach_object_route(
521
+ name: str = Form(...),
522
+ category: str = Form("custom"),
523
+ description: str = Form(""),
524
+ files: list[UploadFile] = File(default=[]),
525
+ ):
526
+ if not name or not name.strip():
527
+ raise HTTPException(status_code=400, detail="Missing required 'name' parameter")
528
+
529
+ cat_val = (category or "custom").strip() or "general"
530
+ desc_val = (description or "").strip()
531
+
532
+ existing = db.get_object_by_name(name.strip())
533
+ obj = existing if existing else db.create_object(name=name.strip(), category=cat_val, description=desc_val)
534
+ added = 0
535
+ for uf in files:
536
+ contents = await uf.read()
537
+ nparr = np.frombuffer(contents, np.uint8)
538
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
539
+ if img is None:
540
+ continue
541
+ emb = orchestrator.embedding_recognizer.extract_embedding_from_image(img)
542
+ if emb is not None:
543
+ db.add_embedding(obj.id, emb.tolist())
544
+ db.add_image_record(obj.id, uf.filename or "image")
545
+ added += 1
546
+ return {"status": "success", "message": f"Taught object '{obj.name}' with {added} embeddings", "id": obj.id}
547
+
548
+
549
+ @app.delete("/api/objects/{object_id}")
550
+ def api_delete_object_route(object_id: str):
551
+ if db.delete_object(object_id):
552
+ return {"status": "deleted"}
553
+ raise HTTPException(status_code=404, detail="Object not found")
554
+
555
+
556
+ @app.get("/api/downloads/export-db")
557
+ def api_export_db_route():
558
+ temp_dir = tempfile.mkdtemp()
559
+ zip_path = os.path.join(temp_dir, "custom_objects_library.zip")
560
+ db.export_database_zip(zip_path)
561
+ return FileResponse(zip_path, filename="custom_objects_library.zip", media_type="application/zip")
562
+
563
+
564
+ @app.get("/api/downloads/requirements")
565
+ def api_download_requirements_route():
566
+ req_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")
567
+ if os.path.exists(req_path):
568
+ return FileResponse(req_path, filename="requirements.txt", media_type="text/plain")
569
+ raise HTTPException(status_code=404, detail="requirements.txt not found")
570
+
571
+
572
+ # Mount Gradio on the FastAPI app
573
+ app = gr.mount_gradio_app(app, demo, path="/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
 
575
  if __name__ == "__main__":
576
+ import uvicorn
577
  port = int(os.environ.get("PORT", os.environ.get("GRADIO_SERVER_PORT", "7860")))
578
+ uvicorn.run(app, host="0.0.0.0", port=port)