Billavenu commited on
Commit
83dad6b
Β·
verified Β·
1 Parent(s): b7bd6cf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -90
app.py CHANGED
@@ -7,7 +7,7 @@ ROCKIT Vision Intelligence β€” Hugging Face Space
7
  GPU-accelerated multimodal search engine.
8
  - Embedding: Qwen3-VL-Embedding (GPU) / CLIP (CPU)
9
  - Search: CAGRA (hipVS) -> PyTorch -> NumPy
10
- - UI: Premium Gradio Demo
11
  """
12
 
13
  import logging
@@ -35,7 +35,7 @@ from ingest import (
35
  from search import search_images, search_videos
36
  import seed_data
37
 
38
- # ── Helpers ──────────────────────────────────────────────────────────────────
39
 
40
  def get_system_info(project: str = DEFAULT_PROJECT) -> str:
41
  img_store = get_store(project, "image_index")
@@ -55,15 +55,17 @@ def get_system_info(project: str = DEFAULT_PROJECT) -> str:
55
  f"| Video Frames | {vid_store.count} | {('VRAM (Hot)' if vid_store.in_vram else 'NVMe (Cold)')} |",
56
  ])
57
 
 
58
  def get_projects_list() -> list[str]:
59
  projects = list_projects()
60
  if DEFAULT_PROJECT not in projects:
61
  projects.insert(0, DEFAULT_PROJECT)
62
  return projects
63
 
64
- # ── Callbacks ────────────────────────────────────────────────────────────────
65
 
66
  def handle_image_upload(files, project, progress=gr.Progress()):
 
67
  if not files:
68
  return "No files uploaded.", get_system_info(project)
69
  results = []
@@ -73,7 +75,9 @@ def handle_image_upload(files, project, progress=gr.Progress()):
73
  results.append(msg)
74
  return "\n".join(results), get_system_info(project)
75
 
 
76
  def handle_video_upload(files, project, progress=gr.Progress()):
 
77
  if not files:
78
  return "No files uploaded.", get_system_info(project)
79
  results = []
@@ -82,25 +86,34 @@ def handle_video_upload(files, project, progress=gr.Progress()):
82
  results.append(msg)
83
  return "\n".join(results), get_system_info(project)
84
 
 
85
  def handle_batch_ingest(project, progress=gr.Progress()):
 
86
  img_count, img_log = ingest_images(project=project, progress_callback=progress)
87
  vid_count, vid_log = ingest_videos(project=project, progress_callback=progress)
88
  log = (
89
  f"=== Batch Ingest Results ===\n\n"
90
- f"Successfully indexed {img_count} images and {vid_count} video frames into project '{project}'."
 
91
  )
92
  return log, get_system_info(project)
93
 
 
94
  def handle_seed(project, progress=gr.Progress()):
 
95
  count, log = seed_data.run(project=project, progress_callback=progress)
96
  return log, get_system_info(project)
97
 
 
98
  def handle_clear(project):
 
99
  get_store(project, "image_index").clear()
100
  get_store(project, "video_index").clear()
101
  return f"All indexes cleared for project '{project}'.", get_system_info(project)
102
 
 
103
  def handle_search(query, mode, top_k, project):
 
104
  if not query.strip():
105
  return "Please enter a search query.", [], ""
106
 
@@ -114,10 +127,9 @@ def handle_search(query, mode, top_k, project):
114
  score = r.get("score", 0)
115
  if path and os.path.exists(path):
116
  gallery_items.append((path, f"{name} (Score: {score:.3f})"))
117
-
118
  return summary, gallery_items, result["store_info"]
119
 
120
- else:
121
  result = search_videos(query, project=project, top_k=int(top_k))
122
  summary = result["llm_summary"]
123
  gallery_items = []
@@ -128,10 +140,11 @@ def handle_search(query, mode, top_k, project):
128
  score = m.get("score", 0)
129
  if path and os.path.exists(path):
130
  gallery_items.append((path, f"{name} @ {time_range} (Score: {score:.3f})"))
131
-
132
  return summary, gallery_items, result["store_info"]
133
 
 
134
  def handle_create_project(name):
 
135
  if not name or not name.strip():
136
  return "Enter a project name.", gr.update()
137
  name = name.strip().lower().replace(" ", "-")
@@ -139,24 +152,29 @@ def handle_create_project(name):
139
  get_project_dir(name)
140
  return f"Project '{name}' created.", gr.update(choices=get_projects_list(), value=name)
141
 
142
- # ── CSS ──────────────────────────────────────────────────────────────────────
 
 
 
 
 
143
 
144
  CSS = """
145
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap');
146
 
147
  body { font-family: 'Inter', sans-serif !important; }
148
 
149
- .gradio-container {
150
- max-width: 1300px !important;
151
- margin: 0 auto !important;
152
  background-color: #050505 !important;
153
  }
154
 
155
  .main-header {
156
  text-align: center;
157
  background: linear-gradient(135deg, #0f0f1b 0%, #1a1a2e 100%);
158
- padding: 3rem 2rem;
159
- border-radius: 24px;
160
  margin-bottom: 2rem;
161
  border: 1px solid rgba(255,255,255,0.05);
162
  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
@@ -173,16 +191,16 @@ body { font-family: 'Inter', sans-serif !important; }
173
 
174
  .main-header h1 {
175
  background: linear-gradient(90deg, #e94560, #a033ff, #4cc9f0);
176
- -webkit-background-clip: text;
177
  -webkit-text-fill-color: transparent;
178
- font-size: 3.2rem !important;
179
  font-weight: 800 !important;
180
  margin: 0;
181
  letter-spacing: -1px;
182
  }
183
 
184
- .main-header p.subtitle {
185
- color: #94a3b8;
186
  font-size: 1.1rem;
187
  margin-top: 0.5rem;
188
  }
@@ -194,9 +212,9 @@ body { font-family: 'Inter', sans-serif !important; }
194
  padding: 1rem !important;
195
  }
196
 
197
- #search-btn {
198
- background: linear-gradient(135deg, #e94560 0%, #533483 100%) !important;
199
- border: none !important;
200
  font-weight: 700 !important;
201
  color: white !important;
202
  transition: all 0.3s ease;
@@ -222,31 +240,47 @@ body { font-family: 'Inter', sans-serif !important; }
222
  footer { display: none !important; }
223
  """
224
 
225
- # ── Build UI ─────────────────────────────────────────────────────────────────
226
 
227
  def build_ui():
228
  logo_path = "assests/rockit_logo.png"
229
  arch_path = "assests/Architecture.svg"
230
  flow_path = "assests/data_flow.svg"
231
- gpu_path = "assests/gpu_compute_tiers.svg"
232
 
233
  with gr.Blocks(
234
  title="ROCKIT Vision Intelligence",
235
- theme=gr.themes.Default(
236
- primary_hue="rose",
237
- secondary_hue="indigo",
 
 
 
238
  neutral_hue="slate",
239
  ),
240
  css=CSS,
241
  ) as app:
242
 
243
- with gr.Div(elem_classes="main-header"):
 
244
  if os.path.exists(logo_path):
245
- gr.Image(logo_path, show_label=False, container=False, width=100, elem_classes="logo-container")
 
 
 
 
 
 
246
  gr.HTML("<h1>ROCKIT Vision Intelligence</h1>")
247
- gr.Markdown("GPU-Accelerated Multimodal Search Platform", elem_classes="subtitle")
 
 
 
248
 
 
249
  with gr.Row():
 
 
250
  with gr.Column(scale=3):
251
  with gr.Group(elem_classes="card"):
252
  gr.Markdown("### πŸ—‚οΈ Project Selection")
@@ -258,85 +292,132 @@ def build_ui():
258
  scale=4,
259
  interactive=True,
260
  )
 
261
  refresh_btn = gr.Button("πŸ”„", scale=1)
262
-
263
  with gr.Accordion("Create New Project", open=False):
264
- new_project_name = gr.Textbox(label="Project ID", placeholder="e.g. security-cam")
 
 
 
265
  create_btn = gr.Button("Initialize Project", variant="secondary")
266
  create_status = gr.Markdown()
267
 
268
- with gr.Group(elem_classes="card", visible=True):
 
269
  gr.Markdown("### βš™οΈ System Status")
270
  system_info = gr.Markdown(value=get_system_info())
271
 
 
272
  with gr.Column(scale=7):
273
  with gr.Tabs():
274
-
275
- # ── Tab 1: Search ──────────────────────────────────────────
276
  with gr.Tab("πŸ” Search"):
277
  with gr.Group(elem_classes="card"):
278
  with gr.Row():
279
  with gr.Column(scale=4):
280
  query_input = gr.Textbox(
281
  label="Natural Language Query",
282
- placeholder='Try "a cat sitting on a laptop" or "someone running in a park"',
 
 
 
283
  lines=2,
284
  )
285
  with gr.Column(scale=1):
286
- search_mode = gr.Radio(["Image Search", "Video Intelligence"], value="Image Search", label="Search Mode")
287
- top_k = gr.Slider(1, 50, value=12, step=1, label="Results Count")
288
-
289
- search_btn = gr.Button("Execute Semantic Search", variant="primary", elem_id="search-btn", size="lg")
290
-
 
 
 
 
 
 
 
 
 
 
 
 
291
  gr.Markdown("### πŸ€– AI Interpretation")
292
- search_summary = gr.Markdown("*Results will appear here...*", elem_classes="card")
293
-
 
 
 
294
  gr.Markdown("### πŸ–ΌοΈ Visual Matches")
295
  result_gallery = gr.Gallery(
296
- label="Retrieved Media",
297
- columns=[3, 4],
298
- rows=[2],
299
- object_fit="contain",
 
300
  height="auto",
301
- elem_classes="gallery-container"
302
  )
303
-
304
  with gr.Accordion("Technical Details", open=False):
305
- store_info = gr.Textbox(label="Vector Store Engine", interactive=False)
 
 
 
306
 
307
- # ── Tab 2: Upload ──────────────────────────────────────────
308
  with gr.Tab("πŸ“€ Ingest Media"):
309
  with gr.Row():
310
  with gr.Column():
311
  with gr.Group(elem_classes="card"):
312
  gr.Markdown("#### πŸ–ΌοΈ Image Ingestion")
313
- img_upload = gr.File(label="Select Images", file_types=["image"], file_count="multiple")
 
 
 
 
 
 
314
  img_btn = gr.Button("Embed & Index Images")
315
- img_log = gr.Textbox(label="Status", lines=4, interactive=False)
316
-
 
 
 
 
317
  with gr.Column():
318
  with gr.Group(elem_classes="card"):
319
  gr.Markdown("#### πŸŽ₯ Video Intelligence")
320
- vid_upload = gr.File(label="Select Videos", file_types=["video"], file_count="multiple")
 
 
 
 
 
321
  vid_btn = gr.Button("Extract & Index Frames")
322
- vid_log = gr.Textbox(label="Status", lines=4, interactive=False)
323
-
 
 
 
 
324
  with gr.Group(elem_classes="card"):
325
  gr.Markdown("#### ⚑ Batch Operations")
326
  with gr.Row():
327
- seed_btn = gr.Button("Seed Demo Data", variant="secondary")
328
- batch_btn = gr.Button("Re-index Folder", variant="secondary")
329
- clear_btn = gr.Button("Purge All Indexes", variant="stop")
330
  action_log = gr.Markdown()
331
 
332
- # ── Tab 3: Workflow ────────────────────────────────────────
333
  with gr.Tab("🧠 How It Works"):
334
  gr.Markdown("""
335
- ### Direct Multimodal Embedding
336
- ROCKIT doesn't use captioning models. It uses **Vision-Language Models (VLM)** to encode visual features
337
- directly into the same vector space as text. This preserves subtle details that text captions often lose.
338
- """)
339
-
340
  with gr.Row():
341
  with gr.Column():
342
  gr.Markdown("#### 1. System Architecture")
@@ -346,9 +427,9 @@ def build_ui():
346
  gr.Markdown("#### 2. Query Flow")
347
  if os.path.exists(flow_path):
348
  gr.Image(flow_path, show_label=False)
349
-
350
  gr.Markdown("---")
351
-
352
  with gr.Row():
353
  with gr.Column():
354
  gr.Markdown("#### 3. GPU Acceleration Tiers")
@@ -356,43 +437,81 @@ def build_ui():
356
  gr.Image(gpu_path, show_label=False)
357
  with gr.Column():
358
  gr.Markdown("""
359
- #### Hot/Cold Memory Management
360
- To support dozens of projects on a single GPU, ROCKIT implements an **NVMe-to-VRAM Async Swap**.
361
-
362
- - **Cold Store (NVMe):** Indexes are serialized as `.cagra` files.
363
- - **Hot Cache (VRAM):** Active projects are copied into VRAM using pinned-memory DMA.
364
- - **LRU Eviction:** Least recently used indexes are purged from VRAM to make room for new ones.
365
- """)
366
-
367
- # Event Bindings
368
- project_select.change(fn=get_system_info, inputs=[project_select], outputs=[system_info])
369
- refresh_btn.click(fn=lambda: gr.update(choices=get_projects_list()), outputs=[project_select])
370
-
 
 
 
 
 
 
 
 
 
371
  create_btn.click(
372
  fn=handle_create_project,
373
  inputs=[new_project_name],
374
  outputs=[create_status, project_select],
375
  )
376
 
 
 
 
 
377
  search_btn.click(
378
- fn=handle_search,
379
- inputs=[query_input, search_mode, top_k, project_select],
380
- outputs=[search_summary, result_gallery, store_info]
381
  )
382
  query_input.submit(
383
- fn=handle_search,
384
- inputs=[query_input, search_mode, top_k, project_select],
385
- outputs=[search_summary, result_gallery, store_info]
 
 
 
 
 
 
 
 
 
 
 
 
386
  )
387
 
388
- img_btn.click(fn=handle_image_upload, inputs=[img_upload, project_select], outputs=[img_log, system_info])
389
- vid_btn.click(fn=handle_video_upload, inputs=[vid_upload, project_select], outputs=[vid_log, system_info])
390
- seed_btn.click(fn=handle_seed, inputs=[project_select], outputs=[action_log, system_info])
391
- batch_btn.click(fn=handle_batch_ingest, inputs=[project_select], outputs=[action_log, system_info])
392
- clear_btn.click(fn=handle_clear, inputs=[project_select], outputs=[action_log, system_info])
 
 
 
 
 
 
 
 
 
 
 
393
 
394
  return app
395
 
 
 
396
  if __name__ == "__main__":
397
  if seed_data.is_needed():
398
  logger.info("Auto-seeding default project from HF Dataset...")
@@ -402,4 +521,4 @@ if __name__ == "__main__":
402
  logger.error(f"Auto-seeding failed: {e}")
403
 
404
  app = build_ui()
405
- app.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
7
  GPU-accelerated multimodal search engine.
8
  - Embedding: Qwen3-VL-Embedding (GPU) / CLIP (CPU)
9
  - Search: CAGRA (hipVS) -> PyTorch -> NumPy
10
+ - UI: Premium Gradio Demo (Gradio >= 4.26, < 5.0)
11
  """
12
 
13
  import logging
 
35
  from search import search_images, search_videos
36
  import seed_data
37
 
38
+ # ── Helpers ───────────────────────────────────────────────────────────────────
39
 
40
  def get_system_info(project: str = DEFAULT_PROJECT) -> str:
41
  img_store = get_store(project, "image_index")
 
55
  f"| Video Frames | {vid_store.count} | {('VRAM (Hot)' if vid_store.in_vram else 'NVMe (Cold)')} |",
56
  ])
57
 
58
+
59
  def get_projects_list() -> list[str]:
60
  projects = list_projects()
61
  if DEFAULT_PROJECT not in projects:
62
  projects.insert(0, DEFAULT_PROJECT)
63
  return projects
64
 
65
+ # ── Callbacks ─────────────────────────────────────────────────────────────────
66
 
67
  def handle_image_upload(files, project, progress=gr.Progress()):
68
+ """Embed and index uploaded images one by one."""
69
  if not files:
70
  return "No files uploaded.", get_system_info(project)
71
  results = []
 
75
  results.append(msg)
76
  return "\n".join(results), get_system_info(project)
77
 
78
+
79
  def handle_video_upload(files, project, progress=gr.Progress()):
80
+ """Extract frames and index uploaded videos."""
81
  if not files:
82
  return "No files uploaded.", get_system_info(project)
83
  results = []
 
86
  results.append(msg)
87
  return "\n".join(results), get_system_info(project)
88
 
89
+
90
  def handle_batch_ingest(project, progress=gr.Progress()):
91
+ """Re-index all images and videos from the project's data folder."""
92
  img_count, img_log = ingest_images(project=project, progress_callback=progress)
93
  vid_count, vid_log = ingest_videos(project=project, progress_callback=progress)
94
  log = (
95
  f"=== Batch Ingest Results ===\n\n"
96
+ f"Successfully indexed {img_count} images and {vid_count} video frames "
97
+ f"into project '{project}'."
98
  )
99
  return log, get_system_info(project)
100
 
101
+
102
  def handle_seed(project, progress=gr.Progress()):
103
+ """Download and seed demo data for the selected project."""
104
  count, log = seed_data.run(project=project, progress_callback=progress)
105
  return log, get_system_info(project)
106
 
107
+
108
  def handle_clear(project):
109
+ """Purge all vector indexes for the selected project."""
110
  get_store(project, "image_index").clear()
111
  get_store(project, "video_index").clear()
112
  return f"All indexes cleared for project '{project}'.", get_system_info(project)
113
 
114
+
115
  def handle_search(query, mode, top_k, project):
116
+ """Run semantic search and return AI summary + gallery items."""
117
  if not query.strip():
118
  return "Please enter a search query.", [], ""
119
 
 
127
  score = r.get("score", 0)
128
  if path and os.path.exists(path):
129
  gallery_items.append((path, f"{name} (Score: {score:.3f})"))
 
130
  return summary, gallery_items, result["store_info"]
131
 
132
+ else: # Video Intelligence
133
  result = search_videos(query, project=project, top_k=int(top_k))
134
  summary = result["llm_summary"]
135
  gallery_items = []
 
140
  score = m.get("score", 0)
141
  if path and os.path.exists(path):
142
  gallery_items.append((path, f"{name} @ {time_range} (Score: {score:.3f})"))
 
143
  return summary, gallery_items, result["store_info"]
144
 
145
+
146
  def handle_create_project(name):
147
+ """Create a new named project workspace."""
148
  if not name or not name.strip():
149
  return "Enter a project name.", gr.update()
150
  name = name.strip().lower().replace(" ", "-")
 
152
  get_project_dir(name)
153
  return f"Project '{name}' created.", gr.update(choices=get_projects_list(), value=name)
154
 
155
+
156
+ def refresh_projects():
157
+ """Return updated dropdown choices."""
158
+ return gr.update(choices=get_projects_list())
159
+
160
+ # ── CSS ───────────────────────────────────────────────────────────────────────
161
 
162
  CSS = """
163
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap');
164
 
165
  body { font-family: 'Inter', sans-serif !important; }
166
 
167
+ .gradio-container {
168
+ max-width: 1300px !important;
169
+ margin: 0 auto !important;
170
  background-color: #050505 !important;
171
  }
172
 
173
  .main-header {
174
  text-align: center;
175
  background: linear-gradient(135deg, #0f0f1b 0%, #1a1a2e 100%);
176
+ padding: 3rem 2rem;
177
+ border-radius: 24px;
178
  margin-bottom: 2rem;
179
  border: 1px solid rgba(255,255,255,0.05);
180
  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
 
191
 
192
  .main-header h1 {
193
  background: linear-gradient(90deg, #e94560, #a033ff, #4cc9f0);
194
+ -webkit-background-clip: text;
195
  -webkit-text-fill-color: transparent;
196
+ font-size: 3.2rem !important;
197
  font-weight: 800 !important;
198
  margin: 0;
199
  letter-spacing: -1px;
200
  }
201
 
202
+ .main-header p.subtitle {
203
+ color: #94a3b8;
204
  font-size: 1.1rem;
205
  margin-top: 0.5rem;
206
  }
 
212
  padding: 1rem !important;
213
  }
214
 
215
+ #search-btn {
216
+ background: linear-gradient(135deg, #e94560 0%, #533483 100%) !important;
217
+ border: none !important;
218
  font-weight: 700 !important;
219
  color: white !important;
220
  transition: all 0.3s ease;
 
240
  footer { display: none !important; }
241
  """
242
 
243
+ # ── Build UI ──────────────────────────────────────────────────────────────────
244
 
245
  def build_ui():
246
  logo_path = "assests/rockit_logo.png"
247
  arch_path = "assests/Architecture.svg"
248
  flow_path = "assests/data_flow.svg"
249
+ gpu_path = "assests/gpu_compute_tiers.svg"
250
 
251
  with gr.Blocks(
252
  title="ROCKIT Vision Intelligence",
253
+ # FIX: gr.themes.Default() was renamed; use gr.themes.Base() or a
254
+ # named preset. Soft() ships with Gradio 4 and takes the same hue
255
+ # kwargs.
256
+ theme=gr.themes.Soft(
257
+ primary_hue="rose",
258
+ secondary_hue="indigo",
259
  neutral_hue="slate",
260
  ),
261
  css=CSS,
262
  ) as app:
263
 
264
+ # ── Header ────────────────────────────────────────────────────────────
265
+ with gr.Column(elem_classes="main-header"):
266
  if os.path.exists(logo_path):
267
+ gr.Image(
268
+ logo_path,
269
+ show_label=False,
270
+ container=False,
271
+ width=100,
272
+ elem_classes="logo-container",
273
+ )
274
  gr.HTML("<h1>ROCKIT Vision Intelligence</h1>")
275
+ gr.Markdown(
276
+ "GPU-Accelerated Multimodal Search Platform",
277
+ elem_classes="subtitle",
278
+ )
279
 
280
+ # ── Main layout ───────────────────────────────────────────────────────
281
  with gr.Row():
282
+
283
+ # Left sidebar
284
  with gr.Column(scale=3):
285
  with gr.Group(elem_classes="card"):
286
  gr.Markdown("### πŸ—‚οΈ Project Selection")
 
292
  scale=4,
293
  interactive=True,
294
  )
295
+ # FIX: outputs must be a list, not a bare component
296
  refresh_btn = gr.Button("πŸ”„", scale=1)
297
+
298
  with gr.Accordion("Create New Project", open=False):
299
+ new_project_name = gr.Textbox(
300
+ label="Project ID",
301
+ placeholder="e.g. security-cam",
302
+ )
303
  create_btn = gr.Button("Initialize Project", variant="secondary")
304
  create_status = gr.Markdown()
305
 
306
+ # FIX: gr.Group does not accept visible= in Gradio 4 β€” removed
307
+ with gr.Group(elem_classes="card"):
308
  gr.Markdown("### βš™οΈ System Status")
309
  system_info = gr.Markdown(value=get_system_info())
310
 
311
+ # Right content area
312
  with gr.Column(scale=7):
313
  with gr.Tabs():
314
+
315
+ # ── Tab 1: Search ─────────────────────────────────────────
316
  with gr.Tab("πŸ” Search"):
317
  with gr.Group(elem_classes="card"):
318
  with gr.Row():
319
  with gr.Column(scale=4):
320
  query_input = gr.Textbox(
321
  label="Natural Language Query",
322
+ placeholder=(
323
+ 'Try "a cat sitting on a laptop" '
324
+ 'or "someone running in a park"'
325
+ ),
326
  lines=2,
327
  )
328
  with gr.Column(scale=1):
329
+ search_mode = gr.Radio(
330
+ ["Image Search", "Video Intelligence"],
331
+ value="Image Search",
332
+ label="Search Mode",
333
+ )
334
+ top_k = gr.Slider(
335
+ 1, 50, value=12, step=1,
336
+ label="Results Count",
337
+ )
338
+
339
+ search_btn = gr.Button(
340
+ "Execute Semantic Search",
341
+ variant="primary",
342
+ elem_id="search-btn",
343
+ size="lg",
344
+ )
345
+
346
  gr.Markdown("### πŸ€– AI Interpretation")
347
+ search_summary = gr.Markdown(
348
+ "*Results will appear here...*",
349
+ elem_classes="card",
350
+ )
351
+
352
  gr.Markdown("### πŸ–ΌοΈ Visual Matches")
353
  result_gallery = gr.Gallery(
354
+ label="Retrieved Media",
355
+ # FIX: columns / rows must be plain int, not list
356
+ columns=4,
357
+ rows=2,
358
+ object_fit="contain",
359
  height="auto",
360
+ elem_classes="gallery-container",
361
  )
362
+
363
  with gr.Accordion("Technical Details", open=False):
364
+ store_info = gr.Textbox(
365
+ label="Vector Store Engine",
366
+ interactive=False,
367
+ )
368
 
369
+ # ── Tab 2: Ingest Media ───────────────────────────────────
370
  with gr.Tab("πŸ“€ Ingest Media"):
371
  with gr.Row():
372
  with gr.Column():
373
  with gr.Group(elem_classes="card"):
374
  gr.Markdown("#### πŸ–ΌοΈ Image Ingestion")
375
+ img_upload = gr.File(
376
+ label="Select Images",
377
+ # FIX: Gradio 4 requires MIME types,
378
+ # not bare category strings
379
+ file_types=["image/*"],
380
+ file_count="multiple",
381
+ )
382
  img_btn = gr.Button("Embed & Index Images")
383
+ img_log = gr.Textbox(
384
+ label="Status",
385
+ lines=4,
386
+ interactive=False,
387
+ )
388
+
389
  with gr.Column():
390
  with gr.Group(elem_classes="card"):
391
  gr.Markdown("#### πŸŽ₯ Video Intelligence")
392
+ vid_upload = gr.File(
393
+ label="Select Videos",
394
+ # FIX: same MIME-type correction
395
+ file_types=["video/*"],
396
+ file_count="multiple",
397
+ )
398
  vid_btn = gr.Button("Extract & Index Frames")
399
+ vid_log = gr.Textbox(
400
+ label="Status",
401
+ lines=4,
402
+ interactive=False,
403
+ )
404
+
405
  with gr.Group(elem_classes="card"):
406
  gr.Markdown("#### ⚑ Batch Operations")
407
  with gr.Row():
408
+ seed_btn = gr.Button("Seed Demo Data", variant="secondary")
409
+ batch_btn = gr.Button("Re-index Folder", variant="secondary")
410
+ clear_btn = gr.Button("Purge All Indexes", variant="stop")
411
  action_log = gr.Markdown()
412
 
413
+ # ── Tab 3: How It Works ───────────────────────────────────
414
  with gr.Tab("🧠 How It Works"):
415
  gr.Markdown("""
416
+ ### Direct Multimodal Embedding
417
+ ROCKIT doesn't use captioning models. It uses **Vision-Language Models (VLM)** to encode
418
+ visual features directly into the same vector space as text. This preserves subtle details
419
+ that text captions often lose.
420
+ """)
421
  with gr.Row():
422
  with gr.Column():
423
  gr.Markdown("#### 1. System Architecture")
 
427
  gr.Markdown("#### 2. Query Flow")
428
  if os.path.exists(flow_path):
429
  gr.Image(flow_path, show_label=False)
430
+
431
  gr.Markdown("---")
432
+
433
  with gr.Row():
434
  with gr.Column():
435
  gr.Markdown("#### 3. GPU Acceleration Tiers")
 
437
  gr.Image(gpu_path, show_label=False)
438
  with gr.Column():
439
  gr.Markdown("""
440
+ #### Hot/Cold Memory Management
441
+ To support dozens of projects on a single GPU, ROCKIT implements an **NVMe-to-VRAM Async Swap**.
442
+
443
+ - **Cold Store (NVMe):** Indexes are serialized as `.cagra` files.
444
+ - **Hot Cache (VRAM):** Active projects are copied into VRAM using pinned-memory DMA.
445
+ - **LRU Eviction:** Least recently used indexes are purged from VRAM to make room for new ones.
446
+ """)
447
+
448
+ # ── Event Bindings ────────────────────────────────────────────────────
449
+
450
+ # Sidebar controls
451
+ project_select.change(
452
+ fn=get_system_info,
453
+ inputs=[project_select],
454
+ outputs=[system_info],
455
+ )
456
+ refresh_btn.click(
457
+ fn=refresh_projects,
458
+ inputs=[],
459
+ outputs=[project_select],
460
+ )
461
  create_btn.click(
462
  fn=handle_create_project,
463
  inputs=[new_project_name],
464
  outputs=[create_status, project_select],
465
  )
466
 
467
+ # Search
468
+ _search_inputs = [query_input, search_mode, top_k, project_select]
469
+ _search_outputs = [search_summary, result_gallery, store_info]
470
+
471
  search_btn.click(
472
+ fn=handle_search,
473
+ inputs=_search_inputs,
474
+ outputs=_search_outputs,
475
  )
476
  query_input.submit(
477
+ fn=handle_search,
478
+ inputs=_search_inputs,
479
+ outputs=_search_outputs,
480
+ )
481
+
482
+ # Ingest
483
+ img_btn.click(
484
+ fn=handle_image_upload,
485
+ inputs=[img_upload, project_select],
486
+ outputs=[img_log, system_info],
487
+ )
488
+ vid_btn.click(
489
+ fn=handle_video_upload,
490
+ inputs=[vid_upload, project_select],
491
+ outputs=[vid_log, system_info],
492
  )
493
 
494
+ # Batch operations
495
+ seed_btn.click(
496
+ fn=handle_seed,
497
+ inputs=[project_select],
498
+ outputs=[action_log, system_info],
499
+ )
500
+ batch_btn.click(
501
+ fn=handle_batch_ingest,
502
+ inputs=[project_select],
503
+ outputs=[action_log, system_info],
504
+ )
505
+ clear_btn.click(
506
+ fn=handle_clear,
507
+ inputs=[project_select],
508
+ outputs=[action_log, system_info],
509
+ )
510
 
511
  return app
512
 
513
+ # ── Entry point ───────────────────────────────────────────────────────────────
514
+
515
  if __name__ == "__main__":
516
  if seed_data.is_needed():
517
  logger.info("Auto-seeding default project from HF Dataset...")
 
521
  logger.error(f"Auto-seeding failed: {e}")
522
 
523
  app = build_ui()
524
+ app.launch(server_name="0.0.0.0", server_port=7860, share=False)