#!/usr/bin/env python3 """ HF_Space_hipVS/app.py ===================== ROCKIT Vision Intelligence — Hugging Face Space GPU-accelerated multimodal search engine. - Embedding: Qwen3-VL-Embedding (GPU) / CLIP (CPU) - Search: CAGRA (hipVS) -> PyTorch -> NumPy - UI: Premium Gradio Demo (Gradio >= 5.7) """ import logging import sys import os from pathlib import Path import gradio as gr sys.path.insert(0, str(Path(__file__).parent)) logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s") logger = logging.getLogger("rockit-vision") from config import ( USE_GPU, EMBED_MODEL, EMBED_DIM, LLM_MODEL, LLM_FALLBACK, FRAME_EVERY_SEC, HF_TOKEN, HF_DATASET_REPO, AUTO_SEED, DEFAULT_PROJECT, DATA_DIR ) from vector_store import get_store, list_projects from ingest import ( ingest_images, ingest_videos, ingest_single_image, ingest_single_video, HAS_FFMPEG, ) from search import search_images, search_videos import seed_data # ── Helpers ─────────────────────────────────────────────────────────────────── def get_system_info(project: str = DEFAULT_PROJECT) -> str: img_store = get_store(project, "image_index") vid_store = get_store(project, "video_index") return "\n".join([ f"### Project Context: `{project}`\n", "| Hardware & Models | Status |", "|:---|:---|", f"| **GPU Acceleration** | {'🚀 Enabled' if USE_GPU else '🐢 Disabled (CPU)'} |", f"| **Search Backend** | {img_store.mode} |", f"| **Vision Model** | `{EMBED_MODEL.split('/')[-1]}` ({EMBED_DIM}d) |", f"| **Reasoning LLM** | `{LLM_MODEL.split('/')[-1]}` |", f"| **Media Engine** | {'ffmpeg detected' if HAS_FFMPEG else 'ffmpeg MISSING'} |", "\n| Index Stats | Count | Location |", "|:---|:---|:---|", f"| Images | {img_store.count} | {('VRAM (Hot)' if img_store.in_vram else 'NVMe (Cold)')} |", f"| Video Frames | {vid_store.count} | {('VRAM (Hot)' if vid_store.in_vram else 'NVMe (Cold)')} |", ]) def get_projects_list() -> list[str]: projects = list_projects() if DEFAULT_PROJECT not in projects: projects.insert(0, DEFAULT_PROJECT) return projects # ── Callbacks ───────────────────────────────────────────────────────────────── def handle_image_upload(files, project, progress=gr.Progress()): """Embed and index uploaded images one by one.""" if not files: return "No files uploaded.", get_system_info(project) results = [] for i, f in enumerate(files): progress((i + 1) / len(files), desc=f"Embedding {Path(f).name}...") ok, msg = ingest_single_image(f, project=project) results.append(msg) return "\n".join(results), get_system_info(project) def handle_video_upload(files, project, progress=gr.Progress()): """Extract frames and index uploaded videos.""" if not files: return "No files uploaded.", get_system_info(project) results = [] for f in files: count, msg = ingest_single_video(f, project=project, progress_callback=progress) results.append(msg) return "\n".join(results), get_system_info(project) def handle_batch_ingest(project, progress=gr.Progress()): """Re-index all images and videos from the project's data folder.""" img_count, img_log = ingest_images(project=project, progress_callback=progress) vid_count, vid_log = ingest_videos(project=project, progress_callback=progress) log = ( f"=== Batch Ingest Results ===\n\n" f"Successfully indexed {img_count} images and {vid_count} video frames " f"into project '{project}'." ) return log, get_system_info(project) def handle_seed(project, progress=gr.Progress()): """Download and seed demo data for the selected project.""" count, log = seed_data.run(project=project, progress_callback=progress) return log, get_system_info(project) def handle_clear(project): """Purge all vector indexes for the selected project.""" get_store(project, "image_index").clear() get_store(project, "video_index").clear() return f"All indexes cleared for project '{project}'.", get_system_info(project) def handle_search(query, mode, top_k, project): """Run semantic search and return AI summary + gallery items.""" if not query.strip(): return "Please enter a search query.", [], "" if mode == "Image Search": result = search_images(query, project=project, top_k=int(top_k)) summary = result["llm_summary"] gallery_items = [] for r in result["results"]: path = r.get("file_path", "") name = r.get("file_name", "Unknown") score = r.get("score", 0) if path and os.path.exists(path): gallery_items.append((path, f"{name} (Score: {score:.3f})")) return summary, gallery_items, result["store_info"] else: # Video Intelligence result = search_videos(query, project=project, top_k=int(top_k)) summary = result["llm_summary"] gallery_items = [] for m in result["matches"]: path = m.get("representative_frame", "") name = m.get("video_name", "Unknown") time_range = f"{m['start']} - {m['end']}" score = m.get("score", 0) if path and os.path.exists(path): gallery_items.append((path, f"{name} @ {time_range} (Score: {score:.3f})")) return summary, gallery_items, result["store_info"] def handle_create_project(name): """Create a new named project workspace.""" if not name or not name.strip(): return "Enter a project name.", gr.skip() name = name.strip().lower().replace(" ", "-") from config import get_project_dir get_project_dir(name) return f"Project '{name}' created.", gr.Dropdown(choices=get_projects_list(), value=name) def refresh_projects(): """Return updated dropdown choices.""" return gr.Dropdown(choices=get_projects_list()) # ── CSS ─────────────────────────────────────────────────────────────────────── CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap'); body { font-family: 'Inter', sans-serif !important; } .gradio-container { max-width: 1300px !important; margin: 0 auto !important; background-color: #050505 !important; } .main-header { text-align: center; background: linear-gradient(135deg, #0f0f1b 0%, #1a1a2e 100%); padding: 3rem 2rem; border-radius: 24px; margin-bottom: 2rem; border: 1px solid rgba(255,255,255,0.05); box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: flex; flex-direction: column; align-items: center; } .logo-container img { max-width: 120px; margin-bottom: 1.5rem; filter: drop-shadow(0 0 15px rgba(233, 69, 96, 0.4)); } .main-header h1 { background: linear-gradient(90deg, #e94560, #a033ff, #4cc9f0); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 3.2rem !important; font-weight: 800 !important; margin: 0; letter-spacing: -1px; } .main-header p.subtitle { color: #94a3b8; font-size: 1.1rem; margin-top: 0.5rem; } .card { background: #11111b !important; border: 1px solid rgba(255,255,255,0.08) !important; border-radius: 16px !important; padding: 1rem !important; } #search-btn { background: linear-gradient(135deg, #e94560 0%, #533483 100%) !important; border: none !important; font-weight: 700 !important; color: white !important; transition: all 0.3s ease; } #search-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(233, 69, 96, 0.4); } .stat-box { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 1rem; border: 1px solid rgba(255,255,255,0.05); } .gallery-container { background: #0a0a0f !important; border-radius: 12px !important; } footer { display: none !important; } """ # ── Build UI ────────────────────────────────────────────────────────────────── def build_ui(): logo_path = "assests/rockit_logo.png" arch_path = "assests/Architecture.svg" flow_path = "assests/data_flow.svg" gpu_path = "assests/gpu_compute_tiers.svg" with gr.Blocks( title="ROCKIT Vision Intelligence", # FIX: gr.themes.Default() was renamed; use gr.themes.Base() or a # named preset. Soft() ships with Gradio 4 and takes the same hue # kwargs. theme=gr.themes.Soft( primary_hue="rose", secondary_hue="indigo", neutral_hue="slate", ), css=CSS, ) as app: # ── Header ──────────────────────────────────────────────────────────── with gr.Column(elem_classes="main-header"): if os.path.exists(logo_path): gr.Image( logo_path, show_label=False, container=False, width=100, elem_classes="logo-container", ) gr.HTML("