muhammadpriv001 commited on
Commit
a56bfc3
·
1 Parent(s): af4128c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1697 -306
app.py CHANGED
@@ -1,120 +1,250 @@
 
 
 
 
 
 
 
 
 
1
  import os
2
  import json
3
  import tempfile
4
- import zipfile
 
5
  import cv2
6
  import numpy as np
7
- import spaces
8
 
9
  from PIL import Image
 
10
  import gradio as gr
11
  from fastapi import FastAPI, File, UploadFile, Form, HTTPException
12
  from fastapi.responses import JSONResponse, FileResponse
13
  from fastapi.middleware.cors import CORSMiddleware
 
14
  import uvicorn
15
 
16
- # ---------------------------------------------------------------------------
17
- # Monkey-patch gradio_client bug (Gradio 4.44.0):
18
- # gradio_client.utils._json_schema_to_python_type crashes with
19
- # TypeError: argument of type 'bool' is not iterable
20
- # when a JSON schema has `additionalProperties: true` (a bool, not a dict).
21
- # ---------------------------------------------------------------------------
22
- import gradio_client.utils as _gc_utils
23
-
24
- _original_json_schema_to_python_type = _gc_utils._json_schema_to_python_type
25
-
26
- def _safe_json_schema_to_python_type(schema, defs=None):
27
- if not isinstance(schema, dict):
28
- return "Any"
29
- return _original_json_schema_to_python_type(schema, defs)
30
-
31
- _gc_utils._json_schema_to_python_type = _safe_json_schema_to_python_type
32
-
33
- # ---------------------------------------------------------------------------
34
- # Starlette / Gradio TemplateResponse compatibility
35
- # ---------------------------------------------------------------------------
36
- # Gradio 4.x uses the older TemplateResponse calling convention while newer
37
- # Starlette releases expect the Request object first. When these versions
38
- # are mixed, Starlette can pass the context dict to Jinja2 as the template
39
- # name, producing: TypeError: unhashable type: 'dict'.
40
- # Normalize the old Gradio call without changing Jinja2 itself.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  try:
 
42
  from starlette.templating import Jinja2Templates
43
 
44
- _original_template_response = Jinja2Templates.TemplateResponse
 
 
 
 
 
 
 
 
45
 
46
- def _compatible_template_response(self, *args, **kwargs):
47
  if (
48
  len(args) >= 2
49
  and isinstance(args[0], dict)
50
  and isinstance(args[1], str)
51
  ):
52
- # New Starlette interprets args[0] as Request. Gradio's older
53
- # call style can provide context first and template name second.
54
  context = args[0]
55
  template_name = args[1]
56
  remaining = args[2:]
 
57
  return _original_template_response(
58
- self, template_name, context, *remaining, **kwargs
 
 
 
 
59
  )
60
- return _original_template_response(self, *args, **kwargs)
61
 
62
- Jinja2Templates.TemplateResponse = _compatible_template_response
63
- except Exception:
64
- # Do not prevent the application from starting if the installed
65
- # Starlette version does not expose this class in the expected form.
66
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  from backend.config import (
69
  KNOWN_CONFIDENCE_THRESHOLD,
70
  OPEN_VOCAB_CONFIDENCE_THRESHOLD,
71
  SPECIFIC_SIMILARITY_THRESHOLD,
72
- SQLITE_DB_PATH
73
  )
 
74
  from backend.ml.orchestrator import orchestrator
75
  from backend.database.storage import db
76
 
77
 
78
-
79
- # ---------------------------------------------------------------------------
80
- # Helper functions (non-GPU)
81
- # ---------------------------------------------------------------------------
82
 
83
  def get_objects_table():
 
84
  objs = db.get_all_objects()
 
85
  data = []
 
86
  for o in objs:
87
- data.append([o.id, o.name, o.category, o.image_count, o.embedding_count, o.created_at])
 
 
 
 
 
 
 
 
 
88
  return data
89
 
90
 
91
  def delete_selected_object(object_id):
92
- if not object_id or not object_id.strip():
93
- return "❌ Please enter a valid Object ID to delete.", get_objects_table()
94
- success = db.delete_object(object_id.strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  if success:
96
- return f"✅ Deleted object '{object_id}'.", get_objects_table()
97
- return f"❌ Object '{object_id}' not found.", get_objects_table()
 
 
 
 
 
 
 
 
98
 
99
 
100
  def load_doc_file(doc_name):
101
- doc_path = os.path.join("docs", f"{doc_name}.md")
 
 
 
 
 
102
  if os.path.exists(doc_path):
103
- with open(doc_path, "r", encoding="utf-8") as f:
 
 
 
 
 
 
104
  return f.read()
105
- return f"Documentation file '{doc_name}' not found."
 
 
 
 
106
 
107
 
108
  def export_db_handler():
 
109
  temp_dir = tempfile.mkdtemp()
110
- export_path = os.path.join(temp_dir, "object_library_export.zip")
111
- db.export_database_zip(export_path)
 
 
 
 
 
 
 
 
112
  return export_path
113
 
114
 
115
- # ---------------------------------------------------------------------------
116
- # GPU Functions — defined BEFORE Gradio Blocks for HF ZeroGPU detection
117
- # ---------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  @spaces.GPU(duration=120)
120
  def run_gradio_detection(
@@ -126,423 +256,1684 @@ def run_gradio_detection(
126
  confidence_val,
127
  similarity_val
128
  ):
 
129
  if input_image is None:
130
- return None, "Please upload or capture an image to perform detection."
131
 
132
- if isinstance(input_image, Image.Image):
133
- img_np = np.array(input_image)
134
- img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  else:
136
- img_bgr = cv2.cvtColor(input_image, cv2.COLOR_RGB2BGR)
137
 
138
- prompts = [p.strip() for p in open_vocab_text.split(",") if p.strip()] if open_vocab_text else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
- annotated_bgr, detections, metadata = orchestrator.process_frame(
141
  image=img_bgr,
 
142
  mode=detection_mode,
 
143
  lock_mode=lock_mode_toggle,
 
144
  lock_target=lock_target_text,
 
145
  open_vocab_prompts=prompts,
 
146
  confidence_thresh=confidence_val,
 
147
  similarity_thresh=similarity_val
148
  )
149
 
150
- annotated_rgb = cv2.cvtColor(annotated_bgr, cv2.COLOR_BGR2RGB)
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  for det in detections:
 
153
  db.log_detection(
 
154
  object_name=det["label"],
155
- detection_type=det.get("type", "known"),
 
 
 
 
 
156
  confidence=det["confidence"]
157
  )
158
 
159
- json_str = json.dumps(metadata, indent=2)
160
- return annotated_rgb, json_str
 
 
 
 
 
 
 
 
 
 
 
 
161
 
 
 
 
162
 
163
  @spaces.GPU(duration=120)
164
- def teach_new_object(name, category, description, reference_images):
165
- if not name or not name.strip():
166
- return "❌ Error: Object name is required.", get_objects_table()
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  if not reference_images:
169
- return "❌ Error: Please upload at least one reference photo.", get_objects_table()
 
 
 
 
 
170
 
171
  try:
172
- existing = db.get_object_by_name(name.strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  if existing:
 
174
  obj = existing
 
175
  else:
176
- obj = db.create_object(name=name.strip(), category=category.strip() or "general", description=description.strip() or "")
 
 
 
 
 
 
 
 
177
 
178
  added_embeddings = 0
 
 
 
 
 
179
  for img_obj in reference_images:
180
- if isinstance(img_obj, str):
 
 
 
 
 
181
  img_path = img_obj
182
- elif hasattr(img_obj, "name"):
 
 
 
 
 
183
  img_path = img_obj.name
 
184
  else:
 
185
  continue
186
 
187
- emb = orchestrator.embedding_recognizer.extract_embedding_from_image_path(img_path)
 
 
 
 
 
 
 
 
 
 
 
188
  if emb is not None:
189
- db.add_embedding(obj.id, emb.tolist())
190
- db.add_image_record(obj.id, img_path)
 
 
 
 
 
 
 
 
 
191
  added_embeddings += 1
192
 
193
- msg = f"✅ Successfully taught object '{obj.name}'! (Added {added_embeddings} reference feature embeddings)"
194
- return msg, get_objects_table()
 
 
 
 
 
 
 
 
 
 
195
  except Exception as e:
196
- return f"❌ Error teaching object: {str(e)}", get_objects_table()
 
 
 
 
197
 
198
 
199
- # ---------------------------------------------------------------------------
200
- # Building Modern Glassmorphic Gradio UI
201
- # ---------------------------------------------------------------------------
202
 
203
  custom_css = """
 
204
  body {
205
  background-color: #0b0f19;
206
  color: #e2e8f0;
207
- font-family: 'Inter', system-ui, -apple-system, sans-serif;
 
 
 
 
208
  }
 
209
  .gradio-container {
210
  max-width: 1280px !important;
211
  margin: 0 auto !important;
212
  }
 
213
  .header-hero {
 
214
  text-align: center;
 
215
  padding: 2.5rem 1rem;
216
- background: linear-gradient(135deg, rgba(30,41,59,0.7) 0%, rgba(15,23,42,0.9) 100%);
 
 
 
 
 
 
 
217
  border-radius: 16px;
218
- border: 1px solid rgba(255, 255, 255, 0.1);
 
 
 
 
219
  backdrop-filter: blur(12px);
 
220
  margin-bottom: 2rem;
221
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.5);
 
 
 
 
 
 
222
  }
 
223
  .header-hero h1 {
 
224
  font-size: 2.5rem;
 
225
  font-weight: 800;
226
- background: linear-gradient(90deg, #60a5fa 0%, #34d399 50%, #f472b6 100%);
 
 
 
 
 
 
 
 
227
  -webkit-background-clip: text;
 
228
  -webkit-text-fill-color: transparent;
 
229
  margin-bottom: 0.5rem;
230
  }
 
231
  .header-hero p {
 
232
  color: #94a3b8;
 
233
  font-size: 1.1rem;
 
234
  max-width: 750px;
 
235
  margin: 0 auto;
236
  }
 
237
  .badge-tag {
 
238
  display: inline-block;
 
239
  padding: 4px 12px;
 
240
  border-radius: 9999px;
 
241
  font-size: 0.8rem;
 
242
  font-weight: 600;
 
243
  margin: 0 4px;
244
- background: rgba(99, 102, 241, 0.2);
 
 
 
245
  color: #818cf8;
246
- border: 1px solid rgba(99, 102, 241, 0.3);
 
 
 
247
  }
 
248
  """
249
 
250
- with gr.Blocks(css=custom_css, title="Open-World Object Intelligence Platform") as demo:
251
 
252
- with gr.Column(elem_classes=["header-hero"]):
253
- gr.HTML("""
254
- <h1>Open-World Object Intelligence Platform</h1>
255
- <p>Unified Multi-Pipeline Vision System powered by <strong>RT-DETR</strong> (Known Objects), <strong>YOLO-World</strong> (Open Vocabulary), and <strong>Visual Embeddings</strong> (Specific Taught Identity).</p>
256
- <div style="margin-top: 1rem;">
257
- <span class="badge-tag">🎯 RT-DETR Known Detector</span>
258
- <span class="badge-tag">🌐 YOLO-World Open Vocab</span>
259
- <span class="badge-tag">🧠 Specific Object Recognition</span>
260
- <span class="badge-tag">🔒 Target Lock Mode</span>
261
- </div>
262
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
  with gr.Tabs():
265
 
266
- # -------------------------------------------------------------------
267
- # TAB 1: Live Testing & Lock Mode
268
- # -------------------------------------------------------------------
269
- with gr.TabItem("👁️ Live Testing & Lock Mode"):
 
 
 
 
270
  with gr.Row():
271
- with gr.Column(scale=1):
272
- input_img = gr.Image(type="pil", label="Input Frame / Image", sources=["upload", "webcam", "clipboard"])
273
 
274
- with gr.Accordion("⚙️ Detection Pipeline Settings", open=True):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  detection_mode_dropdown = gr.Dropdown(
276
- choices=["combined", "known", "open_vocabulary", "specific"],
 
 
 
 
 
 
 
277
  value="combined",
278
- label="Detection Pipeline Mode"
 
 
 
279
  )
 
280
  open_vocab_input = gr.Textbox(
281
- label="Open Vocabulary Prompts (YOLO-World)",
282
- placeholder="e.g. red cup, laptop, screwdriver, wireless mouse",
283
- value="cup, laptop, screwdriver, backpack, bottle"
 
 
 
 
 
 
 
 
 
 
 
 
284
  )
 
285
  confidence_slider = gr.Slider(
286
- minimum=0.1, maximum=1.0, value=KNOWN_CONFIDENCE_THRESHOLD, step=0.05,
287
- label="Detection Confidence Threshold"
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  )
 
289
  similarity_slider = gr.Slider(
290
- minimum=0.1, maximum=1.0, value=SPECIFIC_SIMILARITY_THRESHOLD, step=0.05,
291
- label="Specific Object Similarity Threshold"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  )
293
 
294
- with gr.Accordion("🔒 Lock Mode Controls", open=True):
295
- lock_toggle = gr.Checkbox(label="Enable Lock Mode (Suppress Non-Target Boxes)", value=False)
296
  lock_target_input = gr.Textbox(
 
297
  label="Lock Target",
298
- placeholder="e.g., 'My Cup', 'cup', 'red backpack'",
 
 
 
 
 
299
  value=""
300
  )
301
 
302
- btn_detect = gr.Button("🚀 Run Object Detection", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
- with gr.Column(scale=1):
305
- output_img = gr.Image(label="Annotated Output Feed with Lock Indicator")
306
- json_output = gr.Code(label="Detection Results Metadata (JSON)", language="json")
307
 
308
  btn_detect.click(
 
309
  fn=run_gradio_detection,
 
310
  inputs=[
 
311
  input_img,
 
312
  detection_mode_dropdown,
 
313
  lock_toggle,
 
314
  lock_target_input,
 
315
  open_vocab_input,
 
316
  confidence_slider,
 
317
  similarity_slider
318
  ],
319
- outputs=[output_img, json_output],
 
 
 
 
 
 
 
320
  api_name="detect"
321
  )
322
 
323
- # -------------------------------------------------------------------
324
- # TAB 2: Object Studio (Teach & Manage)
325
- # -------------------------------------------------------------------
326
- with gr.TabItem("🎓 Object Studio (Teach & Manage)"):
327
- gr.Markdown("## Teach Specific Objects to the System")
328
- 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.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
  with gr.Row():
 
 
 
 
 
331
  with gr.Column():
332
- teach_name = gr.Textbox(label="Object Identity Name (e.g. 'My Cup')", placeholder="My Cup")
333
- teach_category = gr.Textbox(label="Category (e.g. 'Drinkware')", placeholder="Drinkware")
334
- teach_desc = gr.Textbox(label="Description", placeholder="Personal ceramic mug with blue handle")
335
- teach_files = gr.File(label="Upload Reference Photos (3-10 photos recommended)", file_count="multiple", file_types=["image"])
336
- btn_teach = gr.Button("🧠 Train & Save Specific Object", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  teach_status = gr.Markdown()
338
 
 
 
 
 
339
  with gr.Column():
340
- gr.Markdown("### Saved Specific Objects Database")
 
 
 
 
341
  objects_df = gr.Dataframe(
342
- headers=["ID", "Name", "Category", "Images", "Embeddings", "Created At"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  value=get_objects_table(),
 
344
  interactive=False
345
  )
346
- del_id_input = gr.Textbox(label="Delete Object ID", placeholder="obj_xxxx")
347
- btn_del = gr.Button("🗑️ Delete Object", variant="stop")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
  btn_teach.click(
 
350
  fn=teach_new_object,
351
- inputs=[teach_name, teach_category, teach_desc, teach_files],
352
- outputs=[teach_status, objects_df],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  api_name="teach_object"
354
  )
355
 
 
 
 
 
356
  btn_del.click(
 
357
  fn=delete_selected_object,
358
- inputs=[del_id_input],
359
- outputs=[teach_status, objects_df]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  )
361
 
362
- # -------------------------------------------------------------------
363
- # TAB 3: Model & Database Downloads
364
- # -------------------------------------------------------------------
365
- with gr.TabItem("💾 Downloads & Package Export"):
366
- gr.Markdown("## Downloads, Models & Custom Object Database Export")
367
- gr.Markdown("Download pretrained models, database export packages, SDK packages, and verified checksum manifests.")
 
368
 
369
  with gr.Row():
 
 
 
 
 
370
  with gr.Column():
371
- gr.Markdown("### 📦 Object Database Export")
372
- btn_export = gr.Button("📥 Export Custom Object Database (.zip)", variant="primary")
373
- export_file_out = gr.File(label="Download Library Archive")
374
- btn_export.click(fn=export_db_handler, inputs=[], outputs=[export_file_out])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
  with gr.Column():
377
- gr.Markdown("### 🤖 Pretrained Vision Models & Manifest")
378
- gr.Markdown("""
379
- | Model | Purpose | SHA-256 Checksum | Format |
380
- | :--- | :--- | :--- | :--- |
381
- | **RT-DETR** | Known Object Detection | `a8f921...c4` | `.pt` (PyTorch) |
382
- | **YOLO-World** | Open-Vocabulary Discovery | `b491a0...f1` | `.pt` (Ultralytics) |
383
- | **ResNet18 Backbone** | Feature Embedding Extractor | `7d88e2...03` | Torchvision |
384
- """)
385
-
386
- # -------------------------------------------------------------------
387
- # TAB 4: Developer Guides & Documentation
388
- # -------------------------------------------------------------------
389
- with gr.TabItem("📚 Developer Guides"):
 
 
 
 
 
 
 
 
 
 
 
 
390
  with gr.Row():
 
391
  doc_selector = gr.Dropdown(
 
392
  choices=[
 
393
  "getting-started",
 
394
  "known-objects",
 
395
  "open-vocabulary",
 
396
  "specific-objects",
 
397
  "lock-mode",
 
398
  "api",
 
399
  "database",
 
400
  "deployment"
401
  ],
 
402
  value="getting-started",
403
- label="Select Documentation Guide"
 
 
 
404
  )
405
- doc_markdown_view = gr.Markdown(value=load_doc_file("getting-started"))
406
-
407
- doc_selector.change(fn=load_doc_file, inputs=[doc_selector], outputs=[doc_markdown_view])
408
-
409
- # -------------------------------------------------------------------
410
- # TAB 5: REST API Reference
411
- # -------------------------------------------------------------------
412
- with gr.TabItem("🔌 REST API"):
413
- gr.Markdown("## REST API & FastAPI Endpoint Specifications")
414
- gr.Markdown("""
415
- ### Available Endpoints:
416
- - **`GET /api/health`**: System status and model check.
417
- - **`POST /api/detect`**: Run detection pipeline on an uploaded image.
418
- - **`GET /api/objects`**: Fetch list of taught objects.
419
- - **`GET /api/downloads/export-db`**: Download object database archive.
420
-
421
- ### Example cURL Request for Lock Mode:
422
- ```bash
423
- curl -X POST "http://localhost:7860/api/detect" \\
424
- -F "file=@sample.jpg" \\
425
- -F "mode=combined" \\
426
- -F "lock_mode=true" \\
427
- -F "lock_target=My Cup"
428
- ```
429
- """)
430
-
431
- # ---------------------------------------------------------------------------
432
- # Launch — use demo.launch() so HF ZeroGPU can detect @spaces.GPU functions
433
- # ---------------------------------------------------------------------------
434
- demo.queue()
435
-
436
- from gradio.routes import App as _GradioApp
437
-
438
- _original_create = _GradioApp.create_app
439
-
440
- @staticmethod
441
- def _patched_create(blocks, **kwargs):
442
- fastapi_app = _original_create(blocks, **kwargs)
443
-
444
- fastapi_app.add_middleware(
445
- CORSMiddleware,
446
- allow_origins=["*"],
447
- allow_credentials=True,
448
- allow_methods=["*"],
449
- allow_headers=["*"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  )
451
 
452
- @fastapi_app.get("/api/health")
453
- def api_health():
454
- return {
455
- "status": "healthy",
456
- "models": {
457
- "rtdetr": orchestrator.rtdetr.model is not None,
458
- "yolo_world": orchestrator.yolo_world.model is not None,
459
- "embedding_recognizer": orchestrator.embedding_recognizer.model is not None,
460
- },
461
- "database": os.path.exists(SQLITE_DB_PATH),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  }
463
 
464
- @fastapi_app.post("/api/detect")
465
- async def api_detect_route(
466
- file: UploadFile = File(...),
467
- mode: str = Form("combined"),
468
- lock_mode: bool = Form(False),
469
- lock_target: str = Form(""),
470
- open_vocab_prompts: str = Form(""),
471
- vocabulary: str = Form(""),
472
- confidence: float = Form(KNOWN_CONFIDENCE_THRESHOLD),
473
- ):
474
- try:
475
- contents = await file.read()
476
- nparr = np.frombuffer(contents, np.uint8)
477
- img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  if img is None:
479
- raise HTTPException(status_code=400, detail="Invalid image file format")
480
- raw_vocab = open_vocab_prompts or vocabulary
481
- prompts = [p.strip() for p in raw_vocab.split(",") if p.strip()] if raw_vocab else None
482
- annotated_frame, detections, metadata = _run_api_detection(
483
- img, mode, lock_mode, lock_target, prompts, confidence
 
 
 
 
 
484
  )
485
- import base64
486
- _, buffer = cv2.imencode(".png", annotated_frame)
487
- img_base64 = base64.b64encode(buffer).decode("utf-8")
488
- return JSONResponse(content={"status": "success", "image_base64": img_base64, "metadata": metadata, "detections": detections})
489
- except Exception as e:
490
- raise HTTPException(status_code=500, detail=str(e))
491
-
492
- @fastapi_app.get("/api/objects")
493
- def api_get_objects_route():
494
- objs = db.get_all_objects()
495
- return [{"id": o.id, "name": o.name, "category": o.category, "images": o.image_count, "image_count": o.image_count} for o in objs]
496
-
497
- @fastapi_app.post("/api/objects")
498
- async def api_teach_object_route(
499
- name: str = Form(...), category: str = Form("custom"),
500
- description: str = Form(""), files: list[UploadFile] = File(default=[]),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  ):
502
- try:
503
- existing = db.get_object_by_name(name.strip())
504
- obj = existing if existing else db.create_object(name=name.strip(), category=category.strip() or "general", description=description.strip() or "")
505
- added = 0
506
- for uf in files:
507
- contents = await uf.read()
508
- nparr = np.frombuffer(contents, np.uint8)
509
- img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
510
- if img is None:
511
- continue
512
- emb = orchestrator.embedding_recognizer.extract_embedding_from_image(img)
513
- if emb is not None:
514
- db.add_embedding(obj.id, emb.tolist())
515
- db.add_image_record(obj.id, uf.filename or "image")
516
- added += 1
517
- return {"status": "success", "message": f"Taught object '{obj.name}' with {added} embeddings", "id": obj.id}
518
- except Exception as e:
519
- raise HTTPException(status_code=500, detail=str(e))
520
-
521
- @fastapi_app.delete("/api/objects/{object_id}")
522
- def api_delete_object_route(object_id: str):
523
- if db.delete_object(object_id):
524
- return {"status": "deleted"}
525
- raise HTTPException(status_code=404, detail="Object not found")
526
-
527
- @fastapi_app.get("/api/downloads/export-db")
528
- def api_export_db_route():
529
- temp_dir = tempfile.mkdtemp()
530
- zip_path = os.path.join(temp_dir, "custom_objects_library.zip")
531
- db.export_database_zip(zip_path)
532
- return FileResponse(zip_path, filename="custom_objects_library.zip", media_type="application/zip")
533
-
534
- @fastapi_app.get("/api/downloads/requirements")
535
- def api_download_requirements_route():
536
- req_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")
537
- if os.path.exists(req_path):
538
- return FileResponse(req_path, filename="requirements.txt", media_type="text/plain")
539
- raise HTTPException(status_code=404, detail="requirements.txt not found")
540
-
541
- return fastapi_app
542
-
543
- _GradioApp.create_app = _patched_create
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
 
545
  if __name__ == "__main__":
546
- demo.launch(server_name="0.0.0.0", server_port=7860)
547
- else:
548
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # OPEN-WORLD OBJECT INTELLIGENCE PLATFORM
3
+ # Hugging Face Spaces + Gradio + ZeroGPU + FastAPI
4
+ # ============================================================
5
+
6
+ # IMPORTANT:
7
+ # Import `spaces` BEFORE GPU/ML-related libraries.
8
+ import spaces
9
+
10
  import os
11
  import json
12
  import tempfile
13
+ import base64
14
+
15
  import cv2
16
  import numpy as np
 
17
 
18
  from PIL import Image
19
+
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
  import uvicorn
26
 
27
+
28
+ # ============================================================
29
+ # GRADIO CLIENT COMPATIBILITY PATCH
30
+ # ============================================================
31
+
32
+ try:
33
+ import gradio_client.utils as _gc_utils
34
+
35
+ if hasattr(
36
+ _gc_utils,
37
+ "_json_schema_to_python_type"
38
+ ):
39
+
40
+ _original_json_schema_to_python_type = (
41
+ _gc_utils._json_schema_to_python_type
42
+ )
43
+
44
+ def _safe_json_schema_to_python_type(
45
+ schema,
46
+ defs=None
47
+ ):
48
+ if not isinstance(schema, dict):
49
+ return "Any"
50
+
51
+ return _original_json_schema_to_python_type(
52
+ schema,
53
+ defs
54
+ )
55
+
56
+ _gc_utils._json_schema_to_python_type = (
57
+ _safe_json_schema_to_python_type
58
+ )
59
+
60
+ except Exception as e:
61
+
62
+ print(
63
+ "[WARNING] Gradio client compatibility patch "
64
+ f"could not be applied: {e}"
65
+ )
66
+
67
+
68
+ # ============================================================
69
+ # STARLETTE / GRADIO TEMPLATE COMPATIBILITY PATCH
70
+ # ============================================================
71
+
72
  try:
73
+
74
  from starlette.templating import Jinja2Templates
75
 
76
+ _original_template_response = (
77
+ Jinja2Templates.TemplateResponse
78
+ )
79
+
80
+ def _compatible_template_response(
81
+ self,
82
+ *args,
83
+ **kwargs
84
+ ):
85
 
 
86
  if (
87
  len(args) >= 2
88
  and isinstance(args[0], dict)
89
  and isinstance(args[1], str)
90
  ):
91
+
 
92
  context = args[0]
93
  template_name = args[1]
94
  remaining = args[2:]
95
+
96
  return _original_template_response(
97
+ self,
98
+ template_name,
99
+ context,
100
+ *remaining,
101
+ **kwargs
102
  )
 
103
 
104
+ return _original_template_response(
105
+ self,
106
+ *args,
107
+ **kwargs
108
+ )
109
+
110
+ Jinja2Templates.TemplateResponse = (
111
+ _compatible_template_response
112
+ )
113
+
114
+ except Exception as e:
115
+
116
+ print(
117
+ "[WARNING] Starlette compatibility patch "
118
+ f"not applied: {e}"
119
+ )
120
+
121
+
122
+ # ============================================================
123
+ # BACKEND IMPORTS
124
+ # ============================================================
125
 
126
  from backend.config import (
127
  KNOWN_CONFIDENCE_THRESHOLD,
128
  OPEN_VOCAB_CONFIDENCE_THRESHOLD,
129
  SPECIFIC_SIMILARITY_THRESHOLD,
130
+ SQLITE_DB_PATH,
131
  )
132
+
133
  from backend.ml.orchestrator import orchestrator
134
  from backend.database.storage import db
135
 
136
 
137
+ # ============================================================
138
+ # GENERAL HELPERS
139
+ # ============================================================
 
140
 
141
  def get_objects_table():
142
+
143
  objs = db.get_all_objects()
144
+
145
  data = []
146
+
147
  for o in objs:
148
+
149
+ data.append([
150
+ o.id,
151
+ o.name,
152
+ o.category,
153
+ o.image_count,
154
+ o.embedding_count,
155
+ o.created_at
156
+ ])
157
+
158
  return data
159
 
160
 
161
  def delete_selected_object(object_id):
162
+
163
+ if (
164
+ not object_id
165
+ or not object_id.strip()
166
+ ):
167
+
168
+ return (
169
+ "❌ Please enter a valid Object ID to delete.",
170
+ get_objects_table()
171
+ )
172
+
173
+ object_id = object_id.strip()
174
+
175
+ success = db.delete_object(
176
+ object_id
177
+ )
178
+
179
  if success:
180
+
181
+ return (
182
+ f"✅ Deleted object '{object_id}'.",
183
+ get_objects_table()
184
+ )
185
+
186
+ return (
187
+ f"❌ Object '{object_id}' not found.",
188
+ get_objects_table()
189
+ )
190
 
191
 
192
  def load_doc_file(doc_name):
193
+
194
+ doc_path = os.path.join(
195
+ "docs",
196
+ f"{doc_name}.md"
197
+ )
198
+
199
  if os.path.exists(doc_path):
200
+
201
+ with open(
202
+ doc_path,
203
+ "r",
204
+ encoding="utf-8"
205
+ ) as f:
206
+
207
  return f.read()
208
+
209
+ return (
210
+ f"Documentation file "
211
+ f"'{doc_name}' not found."
212
+ )
213
 
214
 
215
  def export_db_handler():
216
+
217
  temp_dir = tempfile.mkdtemp()
218
+
219
+ export_path = os.path.join(
220
+ temp_dir,
221
+ "object_library_export.zip"
222
+ )
223
+
224
+ db.export_database_zip(
225
+ export_path
226
+ )
227
+
228
  return export_path
229
 
230
 
231
+ # ============================================================
232
+ # ZERO GPU FUNCTIONS
233
+ # ============================================================
234
+ #
235
+ # IMPORTANT:
236
+ #
237
+ # These MUST be module-level functions.
238
+ #
239
+ # DO NOT put @spaces.GPU inside:
240
+ # - another function
241
+ # - a class
242
+ # - a callback
243
+ # - a dynamically generated function
244
+ #
245
+ # Hugging Face ZeroGPU needs to discover them during startup.
246
+ # ============================================================
247
+
248
 
249
  @spaces.GPU(duration=120)
250
  def run_gradio_detection(
 
256
  confidence_val,
257
  similarity_val
258
  ):
259
+
260
  if input_image is None:
 
261
 
262
+ return (
263
+ None,
264
+ "Please upload or capture an image "
265
+ "to perform detection."
266
+ )
267
+
268
+ # --------------------------------------------------------
269
+ # Convert PIL / NumPy image to OpenCV BGR
270
+ # --------------------------------------------------------
271
+
272
+ if isinstance(
273
+ input_image,
274
+ Image.Image
275
+ ):
276
+
277
+ img_np = np.array(
278
+ input_image
279
+ )
280
+
281
+ img_bgr = cv2.cvtColor(
282
+ img_np,
283
+ cv2.COLOR_RGB2BGR
284
+ )
285
+
286
  else:
 
287
 
288
+ img_bgr = cv2.cvtColor(
289
+ input_image,
290
+ cv2.COLOR_RGB2BGR
291
+ )
292
+
293
+ # --------------------------------------------------------
294
+ # Parse YOLO-World prompts
295
+ # --------------------------------------------------------
296
+
297
+ prompts = None
298
+
299
+ if open_vocab_text:
300
+
301
+ prompts = [
302
+ p.strip()
303
+ for p in open_vocab_text.split(",")
304
+ if p.strip()
305
+ ]
306
+
307
+ # --------------------------------------------------------
308
+ # Run unified vision orchestrator
309
+ # --------------------------------------------------------
310
+
311
+ (
312
+ annotated_bgr,
313
+ detections,
314
+ metadata
315
+ ) = orchestrator.process_frame(
316
 
 
317
  image=img_bgr,
318
+
319
  mode=detection_mode,
320
+
321
  lock_mode=lock_mode_toggle,
322
+
323
  lock_target=lock_target_text,
324
+
325
  open_vocab_prompts=prompts,
326
+
327
  confidence_thresh=confidence_val,
328
+
329
  similarity_thresh=similarity_val
330
  )
331
 
332
+ # --------------------------------------------------------
333
+ # Convert BGR -> RGB
334
+ # --------------------------------------------------------
335
+
336
+ annotated_rgb = cv2.cvtColor(
337
+ annotated_bgr,
338
+ cv2.COLOR_BGR2RGB
339
+ )
340
+
341
+ # --------------------------------------------------------
342
+ # Log detections
343
+ # --------------------------------------------------------
344
 
345
  for det in detections:
346
+
347
  db.log_detection(
348
+
349
  object_name=det["label"],
350
+
351
+ detection_type=det.get(
352
+ "type",
353
+ "known"
354
+ ),
355
+
356
  confidence=det["confidence"]
357
  )
358
 
359
+ # --------------------------------------------------------
360
+ # Metadata JSON
361
+ # --------------------------------------------------------
362
+
363
+ json_str = json.dumps(
364
+ metadata,
365
+ indent=2
366
+ )
367
+
368
+ return (
369
+ annotated_rgb,
370
+ json_str
371
+ )
372
+
373
 
374
+ # ============================================================
375
+ # TEACH SPECIFIC OBJECT - GRADIO
376
+ # ============================================================
377
 
378
  @spaces.GPU(duration=120)
379
+ def teach_new_object(
380
+ name,
381
+ category,
382
+ description,
383
+ reference_images
384
+ ):
385
+
386
+ if (
387
+ not name
388
+ or not name.strip()
389
+ ):
390
+
391
+ return (
392
+ "❌ Error: Object name is required.",
393
+ get_objects_table()
394
+ )
395
 
396
  if not reference_images:
397
+
398
+ return (
399
+ "❌ Error: Please upload at least "
400
+ "one reference photo.",
401
+ get_objects_table()
402
+ )
403
 
404
  try:
405
+
406
+ name = name.strip()
407
+
408
+ category = (
409
+ category.strip()
410
+ if category
411
+ else "general"
412
+ )
413
+
414
+ description = (
415
+ description.strip()
416
+ if description
417
+ else ""
418
+ )
419
+
420
+ # ----------------------------------------------------
421
+ # Existing object or new object
422
+ # ----------------------------------------------------
423
+
424
+ existing = db.get_object_by_name(
425
+ name
426
+ )
427
+
428
  if existing:
429
+
430
  obj = existing
431
+
432
  else:
433
+
434
+ obj = db.create_object(
435
+
436
+ name=name,
437
+
438
+ category=category,
439
+
440
+ description=description
441
+ )
442
 
443
  added_embeddings = 0
444
+
445
+ # ----------------------------------------------------
446
+ # Process reference images
447
+ # ----------------------------------------------------
448
+
449
  for img_obj in reference_images:
450
+
451
+ if isinstance(
452
+ img_obj,
453
+ str
454
+ ):
455
+
456
  img_path = img_obj
457
+
458
+ elif hasattr(
459
+ img_obj,
460
+ "name"
461
+ ):
462
+
463
  img_path = img_obj.name
464
+
465
  else:
466
+
467
  continue
468
 
469
+ # ------------------------------------------------
470
+ # Extract visual embedding
471
+ # ------------------------------------------------
472
+
473
+ emb = (
474
+ orchestrator
475
+ .embedding_recognizer
476
+ .extract_embedding_from_image_path(
477
+ img_path
478
+ )
479
+ )
480
+
481
  if emb is not None:
482
+
483
+ db.add_embedding(
484
+ obj.id,
485
+ emb.tolist()
486
+ )
487
+
488
+ db.add_image_record(
489
+ obj.id,
490
+ img_path
491
+ )
492
+
493
  added_embeddings += 1
494
 
495
+ message = (
496
+ f"✅ Successfully taught object "
497
+ f"'{obj.name}'! "
498
+ f"(Added {added_embeddings} "
499
+ f"reference feature embeddings)"
500
+ )
501
+
502
+ return (
503
+ message,
504
+ get_objects_table()
505
+ )
506
+
507
  except Exception as e:
508
+
509
+ return (
510
+ f"❌ Error teaching object: {str(e)}",
511
+ get_objects_table()
512
+ )
513
 
514
 
515
+ # ============================================================
516
+ # CSS
517
+ # ============================================================
518
 
519
  custom_css = """
520
+
521
  body {
522
  background-color: #0b0f19;
523
  color: #e2e8f0;
524
+ font-family:
525
+ 'Inter',
526
+ system-ui,
527
+ -apple-system,
528
+ sans-serif;
529
  }
530
+
531
  .gradio-container {
532
  max-width: 1280px !important;
533
  margin: 0 auto !important;
534
  }
535
+
536
  .header-hero {
537
+
538
  text-align: center;
539
+
540
  padding: 2.5rem 1rem;
541
+
542
+ background:
543
+ linear-gradient(
544
+ 135deg,
545
+ rgba(30,41,59,0.7) 0%,
546
+ rgba(15,23,42,0.9) 100%
547
+ );
548
+
549
  border-radius: 16px;
550
+
551
+ border:
552
+ 1px solid
553
+ rgba(255, 255, 255, 0.1);
554
+
555
  backdrop-filter: blur(12px);
556
+
557
  margin-bottom: 2rem;
558
+
559
+ box-shadow:
560
+ 0 20px 25px -5px
561
+ rgba(0, 0, 0, 0.5),
562
+
563
+ 0 8px 10px -6px
564
+ rgba(0, 0, 0, 0.5);
565
  }
566
+
567
  .header-hero h1 {
568
+
569
  font-size: 2.5rem;
570
+
571
  font-weight: 800;
572
+
573
+ background:
574
+ linear-gradient(
575
+ 90deg,
576
+ #60a5fa 0%,
577
+ #34d399 50%,
578
+ #f472b6 100%
579
+ );
580
+
581
  -webkit-background-clip: text;
582
+
583
  -webkit-text-fill-color: transparent;
584
+
585
  margin-bottom: 0.5rem;
586
  }
587
+
588
  .header-hero p {
589
+
590
  color: #94a3b8;
591
+
592
  font-size: 1.1rem;
593
+
594
  max-width: 750px;
595
+
596
  margin: 0 auto;
597
  }
598
+
599
  .badge-tag {
600
+
601
  display: inline-block;
602
+
603
  padding: 4px 12px;
604
+
605
  border-radius: 9999px;
606
+
607
  font-size: 0.8rem;
608
+
609
  font-weight: 600;
610
+
611
  margin: 0 4px;
612
+
613
+ background:
614
+ rgba(99, 102, 241, 0.2);
615
+
616
  color: #818cf8;
617
+
618
+ border:
619
+ 1px solid
620
+ rgba(99, 102, 241, 0.3);
621
  }
622
+
623
  """
624
 
 
625
 
626
+ # ============================================================
627
+ # GRADIO APPLICATION
628
+ # ============================================================
629
+
630
+ with gr.Blocks(
631
+ css=custom_css,
632
+ title="Open-World Object Intelligence Platform"
633
+ ) as demo:
634
+
635
+ # ========================================================
636
+ # HEADER
637
+ # ========================================================
638
+
639
+ with gr.Column(
640
+ elem_classes=["header-hero"]
641
+ ):
642
+
643
+ gr.HTML(
644
+ """
645
+ <h1>
646
+ Open-World Object Intelligence Platform
647
+ </h1>
648
+
649
+ <p>
650
+ Unified Multi-Pipeline Vision System powered by
651
+ <strong>RT-DETR</strong>
652
+ (Known Objects),
653
+ <strong>YOLO-World</strong>
654
+ (Open Vocabulary),
655
+ and
656
+ <strong>Visual Embeddings</strong>
657
+ (Specific Taught Identity).
658
+ </p>
659
+
660
+ <div style="margin-top: 1rem;">
661
+
662
+ <span class="badge-tag">
663
+ 🎯 RT-DETR Known Detector
664
+ </span>
665
+
666
+ <span class="badge-tag">
667
+ ��� YOLO-World Open Vocab
668
+ </span>
669
+
670
+ <span class="badge-tag">
671
+ 🧠 Specific Object Recognition
672
+ </span>
673
+
674
+ <span class="badge-tag">
675
+ 🔒 Target Lock Mode
676
+ </span>
677
+
678
+ </div>
679
+ """
680
+ )
681
+
682
+ # ========================================================
683
+ # TABS
684
+ # ========================================================
685
 
686
  with gr.Tabs():
687
 
688
+ # ====================================================
689
+ # TAB 1
690
+ # ====================================================
691
+
692
+ with gr.TabItem(
693
+ "👁️ Live Testing & Lock Mode"
694
+ ):
695
+
696
  with gr.Row():
 
 
697
 
698
+ # ------------------------------------------------
699
+ # INPUT COLUMN
700
+ # ------------------------------------------------
701
+
702
+ with gr.Column(
703
+ scale=1
704
+ ):
705
+
706
+ input_img = gr.Image(
707
+
708
+ type="pil",
709
+
710
+ label=(
711
+ "Input Frame / Image"
712
+ ),
713
+
714
+ sources=[
715
+ "upload",
716
+ "webcam",
717
+ "clipboard"
718
+ ]
719
+ )
720
+
721
+ # --------------------------------------------
722
+ # PIPELINE SETTINGS
723
+ # --------------------------------------------
724
+
725
+ with gr.Accordion(
726
+ "⚙️ Detection Pipeline Settings",
727
+ open=True
728
+ ):
729
+
730
  detection_mode_dropdown = gr.Dropdown(
731
+
732
+ choices=[
733
+ "combined",
734
+ "known",
735
+ "open_vocabulary",
736
+ "specific"
737
+ ],
738
+
739
  value="combined",
740
+
741
+ label=(
742
+ "Detection Pipeline Mode"
743
+ )
744
  )
745
+
746
  open_vocab_input = gr.Textbox(
747
+
748
+ label=(
749
+ "Open Vocabulary Prompts "
750
+ "(YOLO-World)"
751
+ ),
752
+
753
+ placeholder=(
754
+ "e.g. red cup, laptop, "
755
+ "screwdriver, wireless mouse"
756
+ ),
757
+
758
+ value=(
759
+ "cup, laptop, screwdriver, "
760
+ "backpack, bottle"
761
+ )
762
  )
763
+
764
  confidence_slider = gr.Slider(
765
+
766
+ minimum=0.1,
767
+
768
+ maximum=1.0,
769
+
770
+ value=(
771
+ KNOWN_CONFIDENCE_THRESHOLD
772
+ ),
773
+
774
+ step=0.05,
775
+
776
+ label=(
777
+ "Detection Confidence "
778
+ "Threshold"
779
+ )
780
  )
781
+
782
  similarity_slider = gr.Slider(
783
+
784
+ minimum=0.1,
785
+
786
+ maximum=1.0,
787
+
788
+ value=(
789
+ SPECIFIC_SIMILARITY_THRESHOLD
790
+ ),
791
+
792
+ step=0.05,
793
+
794
+ label=(
795
+ "Specific Object "
796
+ "Similarity Threshold"
797
+ )
798
+ )
799
+
800
+ # --------------------------------------------
801
+ # LOCK MODE
802
+ # --------------------------------------------
803
+
804
+ with gr.Accordion(
805
+ "🔒 Lock Mode Controls",
806
+ open=True
807
+ ):
808
+
809
+ lock_toggle = gr.Checkbox(
810
+
811
+ label=(
812
+ "Enable Lock Mode "
813
+ "(Suppress Non-Target Boxes)"
814
+ ),
815
+
816
+ value=False
817
  )
818
 
 
 
819
  lock_target_input = gr.Textbox(
820
+
821
  label="Lock Target",
822
+
823
+ placeholder=(
824
+ "e.g., 'My Cup', "
825
+ "'cup', 'red backpack'"
826
+ ),
827
+
828
  value=""
829
  )
830
 
831
+ btn_detect = gr.Button(
832
+
833
+ "🚀 Run Object Detection",
834
+
835
+ variant="primary"
836
+ )
837
+
838
+ # ------------------------------------------------
839
+ # OUTPUT COLUMN
840
+ # ------------------------------------------------
841
+
842
+ with gr.Column(
843
+ scale=1
844
+ ):
845
+
846
+ output_img = gr.Image(
847
+
848
+ label=(
849
+ "Annotated Output Feed "
850
+ "with Lock Indicator"
851
+ )
852
+ )
853
+
854
+ json_output = gr.Code(
855
+
856
+ label=(
857
+ "Detection Results "
858
+ "Metadata (JSON)"
859
+ ),
860
+
861
+ language="json"
862
+ )
863
 
864
+ # ----------------------------------------------------
865
+ # DETECTION EVENT
866
+ # ----------------------------------------------------
867
 
868
  btn_detect.click(
869
+
870
  fn=run_gradio_detection,
871
+
872
  inputs=[
873
+
874
  input_img,
875
+
876
  detection_mode_dropdown,
877
+
878
  lock_toggle,
879
+
880
  lock_target_input,
881
+
882
  open_vocab_input,
883
+
884
  confidence_slider,
885
+
886
  similarity_slider
887
  ],
888
+
889
+ outputs=[
890
+
891
+ output_img,
892
+
893
+ json_output
894
+ ],
895
+
896
  api_name="detect"
897
  )
898
 
899
+ # ====================================================
900
+ # TAB 2 - OBJECT STUDIO
901
+ # ====================================================
902
+
903
+ with gr.TabItem(
904
+ "🎓 Object Studio (Teach & Manage)"
905
+ ):
906
+
907
+ gr.Markdown(
908
+ "## Teach Specific Objects to the System"
909
+ )
910
+
911
+ gr.Markdown(
912
+ """
913
+ Upload reference photos of your personal
914
+ physical object (e.g. *My Cup*, *Lab Keyring*).
915
+
916
+ The system extracts L2-normalized visual
917
+ embeddings to recognize this exact object identity.
918
+ """
919
+ )
920
 
921
  with gr.Row():
922
+
923
+ # ------------------------------------------------
924
+ # TEACH
925
+ # ------------------------------------------------
926
+
927
  with gr.Column():
928
+
929
+ teach_name = gr.Textbox(
930
+
931
+ label=(
932
+ "Object Identity Name "
933
+ "(e.g. 'My Cup')"
934
+ ),
935
+
936
+ placeholder="My Cup"
937
+ )
938
+
939
+ teach_category = gr.Textbox(
940
+
941
+ label=(
942
+ "Category "
943
+ "(e.g. 'Drinkware')"
944
+ ),
945
+
946
+ placeholder="Drinkware"
947
+ )
948
+
949
+ teach_desc = gr.Textbox(
950
+
951
+ label="Description",
952
+
953
+ placeholder=(
954
+ "Personal ceramic mug "
955
+ "with blue handle"
956
+ )
957
+ )
958
+
959
+ teach_files = gr.File(
960
+
961
+ label=(
962
+ "Upload Reference Photos "
963
+ "(3-10 photos recommended)"
964
+ ),
965
+
966
+ file_count="multiple",
967
+
968
+ file_types=[
969
+ "image"
970
+ ]
971
+ )
972
+
973
+ btn_teach = gr.Button(
974
+
975
+ "🧠 Train & Save Specific Object",
976
+
977
+ variant="primary"
978
+ )
979
+
980
  teach_status = gr.Markdown()
981
 
982
+ # ------------------------------------------------
983
+ # DATABASE
984
+ # ------------------------------------------------
985
+
986
  with gr.Column():
987
+
988
+ gr.Markdown(
989
+ "### Saved Specific Objects Database"
990
+ )
991
+
992
  objects_df = gr.Dataframe(
993
+
994
+ headers=[
995
+
996
+ "ID",
997
+
998
+ "Name",
999
+
1000
+ "Category",
1001
+
1002
+ "Images",
1003
+
1004
+ "Embeddings",
1005
+
1006
+ "Created At"
1007
+ ],
1008
+
1009
  value=get_objects_table(),
1010
+
1011
  interactive=False
1012
  )
1013
+
1014
+ del_id_input = gr.Textbox(
1015
+
1016
+ label="Delete Object ID",
1017
+
1018
+ placeholder="obj_xxxx"
1019
+ )
1020
+
1021
+ btn_del = gr.Button(
1022
+
1023
+ "🗑️ Delete Object",
1024
+
1025
+ variant="stop"
1026
+ )
1027
+
1028
+ # ----------------------------------------------------
1029
+ # TEACH EVENT
1030
+ # ----------------------------------------------------
1031
 
1032
  btn_teach.click(
1033
+
1034
  fn=teach_new_object,
1035
+
1036
+ inputs=[
1037
+
1038
+ teach_name,
1039
+
1040
+ teach_category,
1041
+
1042
+ teach_desc,
1043
+
1044
+ teach_files
1045
+ ],
1046
+
1047
+ outputs=[
1048
+
1049
+ teach_status,
1050
+
1051
+ objects_df
1052
+ ],
1053
+
1054
  api_name="teach_object"
1055
  )
1056
 
1057
+ # ----------------------------------------------------
1058
+ # DELETE EVENT
1059
+ # ----------------------------------------------------
1060
+
1061
  btn_del.click(
1062
+
1063
  fn=delete_selected_object,
1064
+
1065
+ inputs=[
1066
+ del_id_input
1067
+ ],
1068
+
1069
+ outputs=[
1070
+
1071
+ teach_status,
1072
+
1073
+ objects_df
1074
+ ]
1075
+ )
1076
+
1077
+ # ====================================================
1078
+ # TAB 3 - DOWNLOADS
1079
+ # ====================================================
1080
+
1081
+ with gr.TabItem(
1082
+ "💾 Downloads & Package Export"
1083
+ ):
1084
+
1085
+ gr.Markdown(
1086
+ """
1087
+ ## Downloads, Models & Custom Object Database Export
1088
+ """
1089
  )
1090
 
1091
+ gr.Markdown(
1092
+ """
1093
+ Download pretrained models, database export
1094
+ packages, SDK packages, and verified checksum
1095
+ manifests.
1096
+ """
1097
+ )
1098
 
1099
  with gr.Row():
1100
+
1101
+ # ------------------------------------------------
1102
+ # DATABASE EXPORT
1103
+ # ------------------------------------------------
1104
+
1105
  with gr.Column():
1106
+
1107
+ gr.Markdown(
1108
+ "### 📦 Object Database Export"
1109
+ )
1110
+
1111
+ btn_export = gr.Button(
1112
+
1113
+ "📥 Export Custom Object Database (.zip)",
1114
+
1115
+ variant="primary"
1116
+ )
1117
+
1118
+ export_file_out = gr.File(
1119
+
1120
+ label="Download Library Archive"
1121
+ )
1122
+
1123
+ btn_export.click(
1124
+
1125
+ fn=export_db_handler,
1126
+
1127
+ inputs=[],
1128
+
1129
+ outputs=[
1130
+ export_file_out
1131
+ ]
1132
+ )
1133
+
1134
+ # ------------------------------------------------
1135
+ # MODELS
1136
+ # ------------------------------------------------
1137
 
1138
  with gr.Column():
1139
+
1140
+ gr.Markdown(
1141
+ """
1142
+ ### 🤖 Pretrained Vision Models & Manifest
1143
+ """
1144
+ )
1145
+
1146
+ gr.Markdown(
1147
+ """
1148
+ | Model | Purpose | SHA-256 Checksum | Format |
1149
+ | :--- | :--- | :--- | :--- |
1150
+ | **RT-DETR** | Known Object Detection | `a8f921...c4` | `.pt` (PyTorch) |
1151
+ | **YOLO-World** | Open-Vocabulary Discovery | `b491a0...f1` | `.pt` (Ultralytics) |
1152
+ | **ResNet18 Backbone** | Feature Embedding Extractor | `7d88e2...03` | Torchvision |
1153
+ """
1154
+ )
1155
+
1156
+ # ====================================================
1157
+ # TAB 4 - DOCUMENTATION
1158
+ # ====================================================
1159
+
1160
+ with gr.TabItem(
1161
+ "📚 Developer Guides"
1162
+ ):
1163
+
1164
  with gr.Row():
1165
+
1166
  doc_selector = gr.Dropdown(
1167
+
1168
  choices=[
1169
+
1170
  "getting-started",
1171
+
1172
  "known-objects",
1173
+
1174
  "open-vocabulary",
1175
+
1176
  "specific-objects",
1177
+
1178
  "lock-mode",
1179
+
1180
  "api",
1181
+
1182
  "database",
1183
+
1184
  "deployment"
1185
  ],
1186
+
1187
  value="getting-started",
1188
+
1189
+ label=(
1190
+ "Select Documentation Guide"
1191
+ )
1192
  )
1193
+
1194
+ doc_markdown_view = gr.Markdown(
1195
+
1196
+ value=load_doc_file(
1197
+ "getting-started"
1198
+ )
1199
+ )
1200
+
1201
+ doc_selector.change(
1202
+
1203
+ fn=load_doc_file,
1204
+
1205
+ inputs=[
1206
+ doc_selector
1207
+ ],
1208
+
1209
+ outputs=[
1210
+ doc_markdown_view
1211
+ ]
1212
+ )
1213
+
1214
+ # ====================================================
1215
+ # TAB 5 - REST API
1216
+ # ====================================================
1217
+
1218
+ with gr.TabItem(
1219
+ "🔌 REST API"
1220
+ ):
1221
+
1222
+ gr.Markdown(
1223
+ """
1224
+ ## REST API & FastAPI Endpoint Specifications
1225
+
1226
+ ### Available Endpoints
1227
+
1228
+ - **`GET /api/health`**
1229
+ System status and model check.
1230
+
1231
+ - **`POST /api/detect`**
1232
+ Run detection pipeline on an uploaded image.
1233
+
1234
+ - **`GET /api/objects`**
1235
+ Fetch list of taught objects.
1236
+
1237
+ - **`POST /api/objects`**
1238
+ Teach a new object.
1239
+
1240
+ - **`DELETE /api/objects/{object_id}`**
1241
+ Delete a taught object.
1242
+
1243
+ - **`GET /api/downloads/export-db`**
1244
+ Download object database archive.
1245
+
1246
+ - **`GET /api/downloads/requirements`**
1247
+ Download requirements.txt.
1248
+
1249
+ ### Example cURL Request for Lock Mode
1250
+
1251
+ ```bash
1252
+ curl -X POST "http://localhost:7860/api/detect" \\
1253
+ -F "file=@sample.jpg" \\
1254
+ -F "mode=combined" \\
1255
+ -F "lock_mode=true" \\
1256
+ -F "lock_target=My Cup"
1257
+ ```
1258
+ """
1259
+ )
1260
+
1261
+
1262
+ # ============================================================
1263
+ # FASTAPI APPLICATION
1264
+ # ============================================================
1265
+ #
1266
+ # IMPORTANT:
1267
+ #
1268
+ # We keep FastAPI as the parent application and mount Gradio
1269
+ # into it.
1270
+ #
1271
+ # ZeroGPU detects the module-level @spaces.GPU functions above.
1272
+ #
1273
+ # Do NOT put @spaces.GPU around the FastAPI route itself.
1274
+ # ============================================================
1275
+
1276
+ app = FastAPI(
1277
+
1278
+ title=(
1279
+ "Object Intelligence Platform API"
1280
+ ),
1281
+
1282
+ description=(
1283
+ "Open-World Object Intelligence API "
1284
+ "combining RT-DETR, YOLO-World, "
1285
+ "and Specific Object Embeddings."
1286
+ ),
1287
+
1288
+ version="1.0.0"
1289
+ )
1290
+
1291
+
1292
+ # ============================================================
1293
+ # CORS
1294
+ # ============================================================
1295
+
1296
+ app.add_middleware(
1297
+
1298
+ CORSMiddleware,
1299
+
1300
+ allow_origins=["*"],
1301
+
1302
+ allow_credentials=True,
1303
+
1304
+ allow_methods=["*"],
1305
+
1306
+ allow_headers=["*"]
1307
+ )
1308
+
1309
+
1310
+ # ============================================================
1311
+ # HEALTH ENDPOINT
1312
+ # ============================================================
1313
+
1314
+ @app.get(
1315
+ "/api/health"
1316
+ )
1317
+ def api_health():
1318
+
1319
+ return {
1320
+
1321
+ "status": "healthy",
1322
+
1323
+ "models": {
1324
+
1325
+ "rtdetr": (
1326
+ orchestrator.rtdetr.model
1327
+ is not None
1328
+ ),
1329
+
1330
+ "yolo_world": (
1331
+ orchestrator.yolo_world.model
1332
+ is not None
1333
+ ),
1334
+
1335
+ "embedding_recognizer": (
1336
+ orchestrator
1337
+ .embedding_recognizer
1338
+ .model
1339
+ is not None
1340
+ )
1341
+ },
1342
+
1343
+ "database": os.path.exists(
1344
+ SQLITE_DB_PATH
1345
+ )
1346
+ }
1347
+
1348
+
1349
+ # ============================================================
1350
+ # API GPU DETECTION FUNCTION
1351
+ # ============================================================
1352
+
1353
+ @spaces.GPU(duration=120)
1354
+ def _run_api_detection(
1355
+
1356
+ img,
1357
+
1358
+ mode,
1359
+
1360
+ lock_mode,
1361
+
1362
+ lock_target,
1363
+
1364
+ prompts,
1365
+
1366
+ confidence
1367
+
1368
+ ):
1369
+
1370
+ return orchestrator.process_frame(
1371
+
1372
+ image=img,
1373
+
1374
+ mode=mode,
1375
+
1376
+ lock_mode=lock_mode,
1377
+
1378
+ lock_target=lock_target,
1379
+
1380
+ open_vocab_prompts=prompts,
1381
+
1382
+ confidence_thresh=confidence
1383
  )
1384
 
1385
+
1386
+ # ============================================================
1387
+ # POST /api/detect
1388
+ # ============================================================
1389
+
1390
+ @app.post(
1391
+ "/api/detect"
1392
+ )
1393
+ async def api_detect_route(
1394
+
1395
+ file: UploadFile = File(...),
1396
+
1397
+ mode: str = Form(
1398
+ "combined"
1399
+ ),
1400
+
1401
+ lock_mode: bool = Form(
1402
+ False
1403
+ ),
1404
+
1405
+ lock_target: str = Form(
1406
+ ""
1407
+ ),
1408
+
1409
+ open_vocab_prompts: str = Form(
1410
+ ""
1411
+ ),
1412
+
1413
+ vocabulary: str = Form(
1414
+ ""
1415
+ ),
1416
+
1417
+ confidence: float = Form(
1418
+ KNOWN_CONFIDENCE_THRESHOLD
1419
+ )
1420
+ ):
1421
+
1422
+ try:
1423
+
1424
+ # ----------------------------------------------------
1425
+ # Read uploaded image
1426
+ # ----------------------------------------------------
1427
+
1428
+ contents = await file.read()
1429
+
1430
+ nparr = np.frombuffer(
1431
+ contents,
1432
+ np.uint8
1433
+ )
1434
+
1435
+ img = cv2.imdecode(
1436
+ nparr,
1437
+ cv2.IMREAD_COLOR
1438
+ )
1439
+
1440
+ if img is None:
1441
+
1442
+ raise HTTPException(
1443
+
1444
+ status_code=400,
1445
+
1446
+ detail=(
1447
+ "Invalid image file format"
1448
+ )
1449
+ )
1450
+
1451
+ # ----------------------------------------------------
1452
+ # Parse vocabulary
1453
+ # ----------------------------------------------------
1454
+
1455
+ raw_vocab = (
1456
+ open_vocab_prompts
1457
+ or vocabulary
1458
+ )
1459
+
1460
+ prompts = None
1461
+
1462
+ if raw_vocab:
1463
+
1464
+ prompts = [
1465
+
1466
+ p.strip()
1467
+
1468
+ for p in raw_vocab.split(",")
1469
+
1470
+ if p.strip()
1471
+ ]
1472
+
1473
+ # ----------------------------------------------------
1474
+ # GPU inference
1475
+ # ----------------------------------------------------
1476
+
1477
+ (
1478
+ annotated_frame,
1479
+
1480
+ detections,
1481
+
1482
+ metadata
1483
+
1484
+ ) = _run_api_detection(
1485
+
1486
+ img,
1487
+
1488
+ mode,
1489
+
1490
+ lock_mode,
1491
+
1492
+ lock_target,
1493
+
1494
+ prompts,
1495
+
1496
+ confidence
1497
+ )
1498
+
1499
+ # ----------------------------------------------------
1500
+ # Encode output image
1501
+ # ----------------------------------------------------
1502
+
1503
+ success, buffer = cv2.imencode(
1504
+
1505
+ ".png",
1506
+
1507
+ annotated_frame
1508
+ )
1509
+
1510
+ if not success:
1511
+
1512
+ raise HTTPException(
1513
+
1514
+ status_code=500,
1515
+
1516
+ detail=(
1517
+ "Failed to encode "
1518
+ "annotated image."
1519
+ )
1520
+ )
1521
+
1522
+ img_base64 = base64.b64encode(
1523
+ buffer
1524
+ ).decode(
1525
+ "utf-8"
1526
+ )
1527
+
1528
+ return JSONResponse(
1529
+
1530
+ content={
1531
+
1532
+ "status": "success",
1533
+
1534
+ "image_base64": img_base64,
1535
+
1536
+ "metadata": metadata,
1537
+
1538
+ "detections": detections
1539
+ }
1540
+ )
1541
+
1542
+ except HTTPException:
1543
+
1544
+ raise
1545
+
1546
+ except Exception as e:
1547
+
1548
+ raise HTTPException(
1549
+
1550
+ status_code=500,
1551
+
1552
+ detail=str(e)
1553
+ )
1554
+
1555
+
1556
+ # ============================================================
1557
+ # GET /api/objects
1558
+ # ============================================================
1559
+
1560
+ @app.get(
1561
+ "/api/objects"
1562
+ )
1563
+ def api_get_objects_route():
1564
+
1565
+ objs = db.get_all_objects()
1566
+
1567
+ return [
1568
+
1569
+ {
1570
+
1571
+ "id": o.id,
1572
+
1573
+ "name": o.name,
1574
+
1575
+ "category": o.category,
1576
+
1577
+ "images": o.image_count,
1578
+
1579
+ "image_count": o.image_count
1580
  }
1581
 
1582
+ for o in objs
1583
+ ]
1584
+
1585
+
1586
+ # ============================================================
1587
+ # GPU OBJECT TEACHING FUNCTION
1588
+ # ============================================================
1589
+
1590
+ @spaces.GPU(duration=120)
1591
+ def _teach_api_object(
1592
+
1593
+ name,
1594
+
1595
+ category,
1596
+
1597
+ description,
1598
+
1599
+ image_data
1600
+
1601
+ ):
1602
+
1603
+ # --------------------------------------------------------
1604
+ # Find or create object
1605
+ # --------------------------------------------------------
1606
+
1607
+ existing = db.get_object_by_name(
1608
+ name.strip()
1609
+ )
1610
+
1611
+ if existing:
1612
+
1613
+ obj = existing
1614
+
1615
+ else:
1616
+
1617
+ obj = db.create_object(
1618
+
1619
+ name=name.strip(),
1620
+
1621
+ category=(
1622
+ category.strip()
1623
+ or "general"
1624
+ ),
1625
+
1626
+ description=(
1627
+ description.strip()
1628
+ or ""
1629
+ )
1630
+ )
1631
+
1632
+ added = 0
1633
+
1634
+ # --------------------------------------------------------
1635
+ # Extract embeddings
1636
+ # --------------------------------------------------------
1637
+
1638
+ for filename, img in image_data:
1639
+
1640
+ emb = (
1641
+ orchestrator
1642
+ .embedding_recognizer
1643
+ .extract_embedding_from_image(
1644
+ img
1645
+ )
1646
+ )
1647
+
1648
+ if emb is not None:
1649
+
1650
+ db.add_embedding(
1651
+
1652
+ obj.id,
1653
+
1654
+ emb.tolist()
1655
+ )
1656
+
1657
+ db.add_image_record(
1658
+
1659
+ obj.id,
1660
+
1661
+ filename
1662
+ )
1663
+
1664
+ added += 1
1665
+
1666
+ return (
1667
+ obj,
1668
+ added
1669
+ )
1670
+
1671
+
1672
+ # ============================================================
1673
+ # POST /api/objects
1674
+ # ============================================================
1675
+
1676
+ @app.post(
1677
+ "/api/objects"
1678
+ )
1679
+ async def api_teach_object_route(
1680
+
1681
+ name: str = Form(...),
1682
+
1683
+ category: str = Form(
1684
+ "custom"
1685
+ ),
1686
+
1687
+ description: str = Form(
1688
+ ""
1689
+ ),
1690
+
1691
+ files: list[UploadFile] = File(
1692
+ default=[]
1693
+ )
1694
+
1695
+ ):
1696
+
1697
+ try:
1698
+
1699
+ image_data = []
1700
+
1701
+ # ----------------------------------------------------
1702
+ # Read uploaded files
1703
+ # ----------------------------------------------------
1704
+
1705
+ for uf in files:
1706
+
1707
+ contents = await uf.read()
1708
+
1709
+ nparr = np.frombuffer(
1710
+ contents,
1711
+ np.uint8
1712
+ )
1713
+
1714
+ img = cv2.imdecode(
1715
+ nparr,
1716
+ cv2.IMREAD_COLOR
1717
+ )
1718
+
1719
  if img is None:
1720
+
1721
+ continue
1722
+
1723
+ image_data.append(
1724
+
1725
+ (
1726
+ uf.filename or "image",
1727
+
1728
+ img
1729
+ )
1730
  )
1731
+
1732
+ # ----------------------------------------------------
1733
+ # GPU embedding extraction
1734
+ # ----------------------------------------------------
1735
+
1736
+ obj, added = _teach_api_object(
1737
+
1738
+ name,
1739
+
1740
+ category,
1741
+
1742
+ description,
1743
+
1744
+ image_data
1745
+ )
1746
+
1747
+ return {
1748
+
1749
+ "status": "success",
1750
+
1751
+ "message": (
1752
+ f"Taught object "
1753
+ f"'{obj.name}' with "
1754
+ f"{added} embeddings"
1755
+ ),
1756
+
1757
+ "id": obj.id
1758
+ }
1759
+
1760
+ except Exception as e:
1761
+
1762
+ raise HTTPException(
1763
+
1764
+ status_code=500,
1765
+
1766
+ detail=str(e)
1767
+ )
1768
+
1769
+
1770
+ # ============================================================
1771
+ # DELETE /api/objects/{object_id}
1772
+ # ============================================================
1773
+
1774
+ @app.delete(
1775
+ "/api/objects/{object_id}"
1776
+ )
1777
+ def api_delete_object_route(
1778
+ object_id: str
1779
+ ):
1780
+
1781
+ if db.delete_object(
1782
+ object_id
1783
+ ):
1784
+
1785
+ return {
1786
+ "status": "deleted"
1787
+ }
1788
+
1789
+ raise HTTPException(
1790
+
1791
+ status_code=404,
1792
+
1793
+ detail="Object not found"
1794
+ )
1795
+
1796
+
1797
+ # ============================================================
1798
+ # DATABASE EXPORT
1799
+ # ============================================================
1800
+
1801
+ @app.get(
1802
+ "/api/downloads/export-db"
1803
+ )
1804
+ def api_export_db_route():
1805
+
1806
+ temp_dir = tempfile.mkdtemp()
1807
+
1808
+ zip_path = os.path.join(
1809
+
1810
+ temp_dir,
1811
+
1812
+ "custom_objects_library.zip"
1813
+ )
1814
+
1815
+ db.export_database_zip(
1816
+ zip_path
1817
+ )
1818
+
1819
+ return FileResponse(
1820
+
1821
+ zip_path,
1822
+
1823
+ filename=(
1824
+ "custom_objects_library.zip"
1825
+ ),
1826
+
1827
+ media_type=(
1828
+ "application/zip"
1829
+ )
1830
+ )
1831
+
1832
+
1833
+ # ============================================================
1834
+ # REQUIREMENTS DOWNLOAD
1835
+ # ============================================================
1836
+
1837
+ @app.get(
1838
+ "/api/downloads/requirements"
1839
+ )
1840
+ def api_download_requirements_route():
1841
+
1842
+ req_path = os.path.join(
1843
+
1844
+ os.path.dirname(
1845
+ os.path.abspath(__file__)
1846
+ ),
1847
+
1848
+ "requirements.txt"
1849
+ )
1850
+
1851
+ if os.path.exists(
1852
+ req_path
1853
  ):
1854
+
1855
+ return FileResponse(
1856
+
1857
+ req_path,
1858
+
1859
+ filename="requirements.txt",
1860
+
1861
+ media_type="text/plain"
1862
+ )
1863
+
1864
+ raise HTTPException(
1865
+
1866
+ status_code=404,
1867
+
1868
+ detail=(
1869
+ "requirements.txt not found"
1870
+ )
1871
+ )
1872
+
1873
+
1874
+ # ============================================================
1875
+ # MOUNT GRADIO
1876
+ # ============================================================
1877
+ #
1878
+ # Gradio UI is mounted at:
1879
+ #
1880
+ # /
1881
+ #
1882
+ # API remains:
1883
+ #
1884
+ # /api/*
1885
+ # ============================================================
1886
+
1887
+ app = gr.mount_gradio_app(
1888
+
1889
+ app,
1890
+
1891
+ demo,
1892
+
1893
+ path="/"
1894
+ )
1895
+
1896
+
1897
+ # ============================================================
1898
+ # SERVER STARTUP
1899
+ # ============================================================
1900
+ #
1901
+ # IMPORTANT FOR HUGGING FACE SPACES
1902
+ #
1903
+ # Do NOT use:
1904
+ #
1905
+ # uvicorn.run(...)
1906
+ #
1907
+ # when relying on the normal Gradio Space startup.
1908
+ #
1909
+ # Hugging Face launches the application through the Space's
1910
+ # Gradio runtime.
1911
+ #
1912
+ # For local execution, uvicorn is still useful.
1913
+ # ============================================================
1914
 
1915
  if __name__ == "__main__":
1916
+
1917
+ port = int(
1918
+
1919
+ os.environ.get(
1920
+
1921
+ "PORT",
1922
+
1923
+ os.environ.get(
1924
+
1925
+ "GRADIO_SERVER_PORT",
1926
+
1927
+ "7860"
1928
+ )
1929
+ )
1930
+ )
1931
+
1932
+ uvicorn.run(
1933
+
1934
+ app,
1935
+
1936
+ host="0.0.0.0",
1937
+
1938
+ port=port
1939
+ )