nvidia/LocateAnything
496
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>NVIDIA LocateAnything - Fast Vision-Language Grounding</title>7 8 <!-- Premium Google Fonts -->9 <link rel="preconnect" href="https://fonts.googleapis.com">10 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>11 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@500;600;700;800;900&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">12 13 <!-- Tailwind CSS CDN -->14 <script src="https://cdn.tailwindcss.com"></script>15 16 <script>17 tailwind.config = {18 theme: {19 extend: {20 fontFamily: {21 sans: ['Inter', 'sans-serif'],22 outfit: ['Outfit', 'sans-serif'],23 mono: ['Fira Code', 'monospace'],24 },25 colors: {26 nvidia: {27 light: '#76b900',28 brand: '#76b900',29 dark: '#5c9000',30 hover: '#87d300',31 },32 dark: {33 50: '#222222',34 100: '#1a1a1a',35 200: '#121212',36 300: '#0a0a0a',37 400: '#050505',38 }39 }40 }41 }42 }43 </script>44 45 <style>46 body {47 background-color: #050505;48 background-image: 49 radial-gradient(circle at 10% 20%, rgba(118, 185, 0, 0.08) 0%, transparent 45%),50 radial-gradient(circle at 90% 80%, rgba(99, 102, 241, 0.05) 0%, transparent 45%);51 background-attachment: fixed;52 }53 54 /* NVIDIA-style Carbon Triangle Grid Pattern */55 .carbon-grid {56 background-image: 57 linear-gradient(30deg, #0f0f0f 12%, transparent 12.5%, transparent 87%, #0f0f0f 87.5%, #0f0f0f),58 linear-gradient(150deg, #0f0f0f 12%, transparent 12.5%, transparent 87%, #0f0f0f 87.5%, #0f0f0f),59 linear-gradient(30deg, #0f0f0f 12%, transparent 12.5%, transparent 87%, #0f0f0f 87.5%, #0f0f0f),60 linear-gradient(150deg, #0f0f0f 12%, transparent 12.5%, transparent 87%, #0f0f0f 87.5%, #0f0f0f),61 linear-gradient(60deg, #171717 25%, transparent 25.5%, transparent 75%, #171717 75.5%, #171717),62 linear-gradient(60deg, #171717 25%, transparent 25.5%, transparent 75%, #171717 75.5%, #171717);63 background-size: 80px 140px;64 background-position: 0 0, 0 0, 40px 70px, 40px 70px, 0 0, 40px 70px;65 }66 67 /* Glassmorphism Styles */68 .glass-panel {69 background: rgba(18, 18, 18, 0.65);70 backdrop-filter: blur(20px);71 -webkit-backdrop-filter: blur(20px);72 border: 1px solid rgba(255, 255, 255, 0.04);73 box-shadow: 0 24px 64px 0 rgba(0, 0, 0, 0.7);74 }75 76 .glass-panel-interactive {77 transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);78 }79 .glass-panel-interactive:hover {80 border-color: rgba(118, 185, 0, 0.25);81 box-shadow: 0 30px 80px 0 rgba(118, 185, 0, 0.08);82 transform: translateY(-2px);83 }84 85 /* SAM 3 Style Glassmorphic Float Input */86 .sam-input-bar {87 background: rgba(255, 255, 255, 0.06);88 backdrop-filter: blur(25px);89 -webkit-backdrop-filter: blur(25px);90 border: 1px solid rgba(255, 255, 255, 0.08);91 box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);92 transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);93 }94 .sam-input-bar:focus-within {95 background: rgba(255, 255, 255, 0.09);96 border-color: rgba(118, 185, 0, 0.6);97 box-shadow: 0 20px 48px rgba(118, 185, 0, 0.15);98 }99 100 /* Hexagonal Glowing Border for Media Workspace (NVIDIA GTC Keynote Style) */101 .gtc-polygon-wrapper {102 position: relative;103 background: #0f1218;104 border: 1px solid rgba(118, 185, 0, 0.15);105 box-shadow: 0 0 50px rgba(0, 0, 0, 0.8);106 overflow: hidden;107 clip-path: polygon(8% 0%, 100% 0%, 100% 92%, 92% 100%, 0% 100%, 0% 8%);108 }109 .gtc-polygon-wrapper::before {110 content: '';111 position: absolute;112 top: 0;113 left: 0;114 width: 100%;115 height: 100%;116 border: 2px solid #76b900;117 pointer-events: none;118 clip-path: polygon(8% 0%, 100% 0%, 100% 92%, 92% 100%, 0% 100%, 0% 8%);119 opacity: 0.8;120 box-shadow: inset 0 0 20px rgba(118, 185, 0, 0.3);121 }122 123 .gtc-neon-border {124 position: absolute;125 top: -2px;126 left: -2px;127 right: -2px;128 bottom: -2px;129 background: linear-gradient(135deg, #76b900, #3f6200, #76b900);130 z-index: 0;131 pointer-events: none;132 opacity: 0.95;133 clip-path: polygon(8% 0%, 100% 0%, 100% 92%, 92% 100%, 0% 100%, 0% 8%);134 }135 136 .gtc-inner-box {137 position: relative;138 background: #080a0e;139 z-index: 10;140 height: 100%;141 clip-path: polygon(8.1% 0.1%, 99.9% 0.1%, 99.9% 91.9%, 91.9% 99.9%, 0.1% 99.9%, 0.1% 8.1%);142 }143 144 /* Pill Buttons styling */145 .pill-btn-green {146 background-color: #76b900;147 transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);148 }149 .pill-btn-green:hover {150 background-color: #87d300;151 box-shadow: 0 0 24px rgba(118, 185, 0, 0.45);152 transform: translateY(-1px);153 }154 .pill-btn-green:active {155 transform: translateY(1px);156 }157 158 /* Custom Scrollbar */159 ::-webkit-scrollbar {160 width: 6px;161 height: 6px;162 }163 ::-webkit-scrollbar-track {164 background: #0a0a0a;165 }166 ::-webkit-scrollbar-thumb {167 background: #222;168 border-radius: 3px;169 }170 ::-webkit-scrollbar-thumb:hover {171 background: #333;172 }173 174 /* Pulse loaders */175 .dot-pulse {176 animation: pulse 1.4s infinite ease-in-out;177 }178 @keyframes pulse {179 0%, 100% { opacity: 0.3; transform: scale(0.9); }180 50% { opacity: 1; transform: scale(1.1); }181 }182 183 .drop-zone-active {184 border-color: #76b900 !important;185 background: rgba(118, 185, 0, 0.04) !important;186 }187 188 /* Detection overlay tag pop-in (restored from previous demo) */189 @keyframes det-pop {190 0% { opacity: 0; transform: translateY(10px) scale(0.88); }191 60% { opacity: 1; transform: translateY(-2px) scale(1.03); }192 100% { opacity: 1; transform: translateY(0) scale(1); }193 }194 .det-tag-pop {195 opacity: 0;196 animation: det-pop 0.38s cubic-bezier(0.16, 1, 0.3, 1) forwards;197 }198 .det-count-pop {199 animation: det-pop 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;200 }201 202 /* Detected overlays: fixed height, internal scroll */203 .detection-scroll {204 min-height: 0;205 overflow-y: auto;206 overscroll-behavior: contain;207 scroll-behavior: smooth;208 scrollbar-width: thin;209 scrollbar-color: rgba(118, 185, 0, 0.45) rgba(0, 0, 0, 0.2);210 mask-image: linear-gradient(to bottom, black 88%, transparent 100%);211 -webkit-mask-image: linear-gradient(to bottom, black 88%, transparent 100%);212 }213 .detection-scroll::-webkit-scrollbar {214 width: 5px;215 }216 .detection-scroll::-webkit-scrollbar-thumb {217 background: rgba(118, 185, 0, 0.45);218 border-radius: 999px;219 }220 </style>221</head>222<body class="text-slate-100 font-sans min-h-screen pb-16 carbon-grid">223 224 <!-- NVIDIA Brand Navigation Header (Transparent dark blur) -->225 <nav class="bg-black/40 backdrop-blur-md sticky top-0 z-50 px-6 py-3.5 border-b border-white/5 shadow-lg">226 <div class="max-w-[1600px] mx-auto flex items-center justify-between">227 <!-- Official Styled NVIDIA Brand Text Logo -->228 <a href="#" class="flex items-center gap-1.5 select-none group">229 <svg class="h-6 w-6 text-nvidia-brand transition-transform duration-500 group-hover:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">230 <path stroke-linecap="round" stroke-linejoin="round" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2z" />231 </svg>232 <span class="font-outfit text-[22px] font-black tracking-tighter text-white">233 NVIDIA <span class="font-light tracking-wide text-slate-400">LocateAnything</span>234 </span>235 </a>236 237 <span class="px-3 py-1 text-xs font-semibold rounded bg-nvidia-brand/10 text-nvidia-brand border border-nvidia-brand/20 flex items-center gap-1.5 font-mono">238 <span class="h-1.5 w-1.5 rounded-full bg-nvidia-brand animate-pulse"></span>239 ZeroGPU Server240 </span>241 </div>242 </nav>243 244 <!-- MAIN MINIMAL LAYOUT CONTAINER -->245 <main class="max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8 pt-8 lg:pt-10 space-y-8">246 247 <!-- Giant Showcase Container (SAM 3 Full-Bleed Style) -->248 <div class="relative w-full rounded-[32px] overflow-hidden border border-white/5 bg-[#080a0e] shadow-2xl h-[580px] lg:h-[640px] flex select-none">249 250 <!-- 1. Dedicated Media Canvas (offset right of the control panel on desktop) -->251 <div class="absolute inset-y-0 right-0 left-0 lg:left-[440px] z-0 flex items-center justify-center bg-black/40 lg:border-l lg:border-white/10">252 <!-- Drop Zone (Initially shown) -->253 <div id="drop-zone" class="absolute inset-0 border-none rounded-none bg-transparent flex flex-col items-center justify-center p-4 text-center cursor-pointer transition-all z-10">254 <div id="upload-prompt" class="space-y-3 opacity-60 hover:opacity-100 transition-opacity">255 <div class="inline-flex h-12 w-12 rounded-full bg-white/5 items-center justify-center text-slate-300">256 <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">257 <path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />258 </svg>259 </div>260 <div>261 <p class="text-xs font-bold text-slate-200">Drag & drop your file here</p>262 <p class="text-[10px] text-slate-500 mt-1">or click to browse local folders</p>263 </div>264 </div>265 266 <!-- Dynamic Preview Media -->267 <img id="preview-image" src="" alt="Input Preview" class="hidden max-h-full max-w-full rounded-2xl object-contain shadow-2xl z-20 border border-white/5">268 <video id="preview-video" src="" controls class="hidden max-h-full max-w-full rounded-2xl object-contain shadow-2xl z-20 border border-white/5"></video>269 270 <!-- File Input -->271 <input type="file" id="media-file-input" accept="image/*,video/*" class="absolute inset-0 opacity-0 cursor-pointer z-30">272 </div>273 274 <!-- Inference Output Zone -->275 <div class="absolute inset-0 pointer-events-none flex items-center justify-center z-20">276 <img id="output-image" src="" alt="Inference Output" class="hidden max-h-full max-w-full rounded-2xl object-contain shadow-2xl pointer-events-auto border border-white/5">277 <video id="output-video" src="" controls class="hidden max-h-full max-w-full rounded-2xl object-contain shadow-2xl pointer-events-auto border border-white/5"></video>278 </div>279 280 <!-- Processing Overlays -->281 <div id="processing-overlay" class="absolute inset-0 bg-black/85 backdrop-blur-sm hidden flex-col items-center justify-center gap-4 z-40">282 <div class="flex gap-1.5">283 <span class="dot-pulse inline-block h-3 w-3 rounded-full bg-nvidia-brand" style="animation-delay: 0s;"></span>284 <span class="dot-pulse inline-block h-3 w-3 rounded-full bg-emerald-400" style="animation-delay: 0.2s;"></span>285 <span class="dot-pulse inline-block h-3 w-3 rounded-full bg-emerald-300" style="animation-delay: 0.4s;"></span>286 </div>287 <div class="text-center space-y-1">288 <p id="processing-status" class="text-[11px] font-bold tracking-widest text-slate-200 uppercase">Executing Model...</p>289 <p class="text-[9px] text-slate-500 uppercase tracking-wider font-mono">ZeroGPU Queue Active</p>290 </div>291 </div>292 </div>293 294 <!-- 2. Left Control Panel (Title, simple selectors, accordion, and action buttons) -->295 <div class="absolute left-6 top-8 bottom-8 z-30 flex flex-col justify-between w-[380px] max-w-[calc(100%-3rem)] pointer-events-none">296 297 <!-- Main Header Overlay text -->298 <div class="space-y-3 pt-4 pointer-events-auto bg-gradient-to-b from-[#080a0e]/90 via-[#080a0e]/60 to-transparent p-4 rounded-2xl">299 <span class="text-[9px] font-bold text-nvidia-brand uppercase tracking-widest block font-mono">AI Research from NVIDIA</span>300 <h1 class="font-outfit text-3xl sm:text-5xl font-black tracking-tight text-white leading-none">301 Locate<span class="text-nvidia-brand font-light">Anything</span>302 </h1>303 <p class="text-xs text-slate-400 max-w-sm font-medium leading-relaxed">304 NVIDIA's advanced 3B vision-language model. Locate any object, UI target, or text in images and videos with natural language.305 </p>306 <p class="text-[9px] text-slate-500 max-w-sm leading-relaxed border-l-2 border-nvidia-brand/30 pl-2.5">307 Note: inputs larger than 1K are auto-resized in this Space demo. For full-resolution inference, download the weights and run locally.308 </p>309 </div>310 311 <!-- Setup Glass Card Controls -->312 <div class="glass-panel rounded-2xl p-4 space-y-4 pointer-events-auto max-w-xs shadow-2xl">313 <div class="grid grid-cols-2 gap-3">314 315 <!-- Media Type toggle selection -->316 <div class="space-y-1">317 <label class="text-[8px] font-bold text-slate-400 uppercase tracking-widest">Media Type</label>318 <div class="grid grid-cols-2 gap-0.5 bg-black/40 p-0.5 rounded-lg border border-white/5 text-center">319 <button id="media-type-image" class="py-1 rounded-md font-semibold text-[9px] transition-all bg-nvidia-brand text-black font-outfit font-black shadow shadow-nvidia-brand/10">320 Image321 </button>322 <button id="media-type-video" class="py-1 rounded-md font-semibold text-[9px] text-slate-400 hover:text-slate-200 transition-all">323 Video324 </button>325 </div>326 </div>327 328 <!-- Task Selector -->329 <div class="space-y-1">330 <label for="task-type" class="text-[8px] font-bold text-slate-400 uppercase tracking-widest">Task Type</label>331 <select id="task-type" class="w-full bg-black/40 border border-white/5 rounded-lg px-2 py-1 text-[9px] focus:border-nvidia-brand focus:outline-none transition-all text-slate-200 font-semibold">332 <option value="Detection">Detection</option>333 <option value="Grounding">Grounding</option>334 <option value="OCR">OCR</option>335 <option value="GUI">GUI</option>336 <option value="Pointing">Pointing</option>337 </select>338 </div>339 340 </div>341 342 <!-- Advanced parameters sliders (Collapsible details inside the left overlay) -->343 <details class="group border-t border-white/5 pt-3">344 <summary class="list-none flex justify-between items-center cursor-pointer select-none text-[8px] font-bold text-slate-400 tracking-wider uppercase hover:text-slate-200 transition-colors">345 <span>โ๏ธ Advanced parameters</span>346 <svg class="h-3 w-3 transform group-open:rotate-180 transition-transform text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">347 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />348 </svg>349 </summary>350 <div class="space-y-3 pt-3">351 352 <!-- Inference Mode Selection -->353 <div class="space-y-1">354 <label for="inference-mode" class="text-[8px] font-bold text-slate-400 uppercase tracking-widest">Inference Mode</label>355 <select id="inference-mode" class="w-full bg-black/40 border border-white/5 rounded-lg px-2 py-1 text-[9px] focus:border-nvidia-brand focus:outline-none transition-all text-slate-200">356 <option value="hybrid">Hybrid</option>357 <option value="fast">Fast</option>358 <option value="slow">Slow</option>359 </select>360 </div>361 362 <!-- Short side resize cap -->363 <div class="space-y-1">364 <label for="short-size" class="text-[8px] font-bold text-slate-400 uppercase tracking-widest">Resize Cap (px)</label>365 <input type="number" id="short-size" placeholder="Auto-Cap (1024)" class="w-full bg-black/40 border border-white/5 rounded-lg px-2 py-1 text-[9px] focus:border-nvidia-brand focus:outline-none transition-all text-slate-200 font-mono">366 </div>367 368 <!-- Temp -->369 <div class="space-y-1">370 <div class="flex justify-between text-[8px] uppercase font-bold text-slate-400 tracking-wider">371 <span>Temperature</span>372 <span id="temp-val" class="font-mono text-nvidia-brand">0.7</span>373 </div>374 <input type="range" id="temp" min="0.1" max="2.0" step="0.1" value="0.7" class="w-full h-0.5 bg-black rounded appearance-none cursor-pointer accent-nvidia-brand">375 </div>376 377 <!-- Top P -->378 <div class="space-y-1">379 <div class="flex justify-between text-[8px] uppercase font-bold text-slate-400 tracking-wider">380 <span>Top P</span>381 <span id="topp-val" class="font-mono text-nvidia-brand">0.9</span>382 </div>383 <input type="range" id="topp" min="0.05" max="1.0" step="0.05" value="0.9" class="w-full h-0.5 bg-black rounded appearance-none cursor-pointer accent-nvidia-brand">384 </div>385 386 <!-- Top K -->387 <div class="space-y-1">388 <div class="flex justify-between text-[8px] uppercase font-bold text-slate-400 tracking-wider">389 <span>Top K</span>390 <span id="topk-val" class="font-mono text-nvidia-brand">20</span>391 </div>392 <input type="range" id="topk" min="1" max="100" step="1" value="20" class="w-full h-0.5 bg-black rounded appearance-none cursor-pointer accent-nvidia-brand">393 </div>394 395 <!-- Video Frames (Only displayed for Video mode) -->396 <div id="video-frames-wrapper" class="space-y-1 opacity-50 pointer-events-none transition-opacity duration-300">397 <div class="flex justify-between text-[8px] uppercase font-bold text-slate-400 tracking-wider">398 <span>Max Video Frames</span>399 <span id="frames-val" class="font-mono text-nvidia-brand">4</span>400 </div>401 <input type="range" id="max-frames" min="1" max="10" step="1" value="4" class="w-full h-0.5 bg-black rounded appearance-none cursor-pointer accent-nvidia-brand" disabled>402 </div>403 404 </div>405 </details>406 407 <!-- Quick Start Guide -->408 <details class="group border-t border-white/5 pt-3" open>409 <summary class="list-none flex justify-between items-center cursor-pointer select-none text-[8px] font-bold text-nvidia-brand tracking-wider uppercase hover:text-nvidia-hover transition-colors">410 <span>๐ How to Use</span>411 <svg class="h-3 w-3 transform group-open:rotate-180 transition-transform text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">412 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />413 </svg>414 </summary>415 <ol class="space-y-1.5 pt-2.5 text-[9px] text-slate-400 leading-relaxed list-decimal list-inside marker:text-nvidia-brand/70">416 <li>Upload an <strong class="text-slate-300">Image</strong> or <strong class="text-slate-300">Video</strong>, or pick a Quick Sandbox example below.</li>417 <li>Choose a <strong class="text-slate-300">Task Type</strong>: Detection ยท Grounding ยท OCR ยท GUI ยท Pointing.</li>418 <li>Enter <strong class="text-slate-300">Categories</strong> in the search bar (comma-separated, e.g. <code class="text-nvidia-brand/80">car, person</code>).</li>419 <li>Optionally tune <strong class="text-slate-300">Advanced parameters</strong> above (mode, resize, temperature, etc.).</li>420 <li>Click <strong class="text-nvidia-brand">Run Inference</strong> or press <kbd class="px-1 py-0.5 rounded bg-white/5 border border-white/10 text-[8px]">Enter</kbd> in the search bar.</li>421 </ol>422 </details>423 </div>424 425 <!-- CTA Action Button (Floats at bottom-left corner of visual container) -->426 <div class="pointer-events-auto pt-2 max-w-xs">427 <button id="run-btn" class="pill-btn-green w-full py-3 px-6 rounded-full text-black font-extrabold text-sm flex items-center justify-center gap-2 select-none shadow-2xl">428 <span id="btn-icon">๐ง </span>429 <span id="btn-text">Run Inference</span>430 </button>431 </div>432 433 </div>434 435 <!-- 3. Floating Categories Search Bar (bottom-center of the dedicated image zone) -->436 <div class="absolute bottom-6 left-0 right-0 lg:left-[440px] z-30 flex flex-col items-center gap-2 px-6 pointer-events-none">437 <div class="sam-input-bar rounded-2xl px-3.5 py-2.5 flex items-center gap-2 w-full max-w-md pointer-events-auto">438 <svg class="h-4 w-4 text-nvidia-brand shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">439 <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />440 </svg>441 <input type="text" id="categories" value="car, bus, person, potted plant" placeholder="Describe objects to locate..." class="bg-transparent border-none outline-none focus:outline-none w-full text-slate-100 placeholder-slate-600 font-semibold text-xs">442 <button id="clear-search-btn" class="text-slate-500 hover:text-white transition-colors p-0.5 rounded-full hover:bg-white/5 shrink-0">443 <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">444 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />445 </svg>446 </button>447 </div>448 <p class="text-[9px] text-slate-500 text-center leading-relaxed pointer-events-none px-1 max-w-md">449 Comma-separated targets ยท supports English & Chinese ยท press <span class="text-slate-400">Enter</span> to run450 </p>451 </div>452 453 <!-- Floating Workspace Status (top-right of the image zone) -->454 <div class="absolute top-4 right-4 z-30 bg-black/60 backdrop-blur px-2.5 py-1 rounded-lg border border-white/10 text-[9px] text-slate-400 font-mono select-none pointer-events-none">455 status: <span id="workspace-status" class="text-slate-200 font-semibold">No Media Loaded</span>456 </div>457 458 </div>459 460 <!-- Shelf Section (Examples and Log metrics placed directly below the giant showcase) -->461 <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">462 463 <!-- Left: Examples Library Shelf (Col Span: 5) -->464 <div class="lg:col-span-5 space-y-4">465 <div class="glass-panel rounded-2xl p-5 space-y-4">466 <span class="text-[9px] font-bold text-slate-400 uppercase tracking-widest block font-mono">๐ผ๏ธ Interactive Quick Sandbox</span>467 <div class="grid grid-cols-4 gap-3">468 469 <!-- Card 1 -->470 <div class="example-card border border-white/5 rounded-xl p-1 cursor-pointer group space-y-1 bg-black/35 hover:border-nvidia-brand/20 transition-all text-center" data-type="Image" data-name="Book" data-category="book" data-task="Detection" data-mode="hybrid" data-asset="assets/book.jpg">471 <div class="h-12 w-full rounded-lg bg-cover bg-center overflow-hidden bg-slate-900" style="background-image: url('/assets/book.jpg');"></div>472 <span class="text-[9px] font-semibold text-slate-300 block truncate">Book</span>473 </div>474 475 <!-- Card 2 -->476 <div class="example-card border border-white/5 rounded-xl p-1 cursor-pointer group space-y-1 bg-black/35 hover:border-nvidia-brand/20 transition-all text-center" data-type="Image" data-name="Sushi" data-category="sushi" data-task="Detection" data-mode="hybrid" data-asset="assets/sweet.jpg">477 <div class="h-12 w-full rounded-lg bg-cover bg-center overflow-hidden bg-slate-900" style="background-image: url('/assets/sweet.jpg');"></div>478 <span class="text-[9px] font-semibold text-slate-300 block truncate">Sushi</span>479 </div>480 481 <!-- Card 3 -->482 <div class="example-card border border-white/5 rounded-xl p-1 cursor-pointer group space-y-1 bg-black/35 hover:border-nvidia-brand/20 transition-all text-center" data-type="Image" data-name="Person" data-category="person" data-task="Detection" data-mode="hybrid" data-asset="assets/person.jpg">483 <div class="h-12 w-full rounded-lg bg-cover bg-center overflow-hidden bg-slate-900" style="background-image: url('/assets/person.jpg');"></div>484 <span class="text-[9px] font-semibold text-slate-300 block truncate">People</span>485 </div>486 487 <!-- Card 4 -->488 <div class="example-card border border-white/5 rounded-xl p-1 cursor-pointer group space-y-1 bg-black/35 hover:border-nvidia-brand/20 transition-all text-center" data-type="Image" data-name="OCR" data-category="text" data-task="OCR" data-mode="slow" data-asset="assets/ocr.jpg">489 <div class="h-12 w-full rounded-lg bg-cover bg-center overflow-hidden bg-slate-900" style="background-image: url('/assets/ocr.jpg');"></div>490 <span class="text-[9px] font-semibold text-slate-300 block truncate">OCR</span>491 </div>492 493 </div>494 </div>495 496 <!-- Text Prompt logs -->497 <div class="glass-panel rounded-2xl p-4 text-[10px] text-slate-500 font-mono flex justify-between items-center select-none bg-black/40">498 <span class="truncate block">compiled: <span id="raw-prompt-preview" class="text-slate-400"></span></span>499 </div>500 </div>501 502 <!-- Right: Performance Metrics & Tag draw overlays (Col Span: 7) -->503 <div class="lg:col-span-7 space-y-4">504 <div class="glass-panel rounded-2xl p-5 space-y-4">505 <div class="grid grid-cols-1 sm:grid-cols-12 gap-4 items-start">506 507 <!-- Performance Statistics Metrics Console (Grid: 5) -->508 <div class="sm:col-span-5 bg-black/60 rounded-xl p-4 border border-white/5 font-mono text-[10px] text-slate-300 space-y-2 leading-normal h-[168px]">509 <div class="text-nvidia-brand font-bold border-b border-white/5 pb-1 mb-1.5 uppercase tracking-widest text-[9px] font-mono">๐ Metrics Log</div>510 <div class="flex justify-between"><span class="text-slate-500">Status:</span> <span id="meta-status" class="text-emerald-500 font-semibold">Idle</span></div>511 <div class="flex justify-between"><span class="text-slate-500">Tokens/Frames:</span> <span id="meta-tokens">-</span></div>512 <div class="flex justify-between"><span class="text-slate-500">Detections:</span> <span id="meta-boxes">-</span></div>513 <div class="flex justify-between"><span class="text-slate-500">TPS / BPS:</span> <span><span id="meta-tps">-</span> / <span id="meta-bps">-</span></span></div>514 <div class="flex justify-between"><span class="text-slate-500">Time:</span> <span id="meta-time">-</span></div>515 </div>516 517 <!-- Tag drawer box list (Grid: 7) -->518 <div class="sm:col-span-7 bg-black/60 rounded-xl p-4 border border-white/5 flex flex-col h-[168px] overflow-hidden">519 <div class="text-nvidia-brand font-mono font-bold border-b border-white/5 pb-1 mb-2 uppercase tracking-widest text-[9px] flex justify-between shrink-0">520 <span>๐ฏ Detected Target Overlays</span>521 <span id="detection-count-badge" class="text-[8px] bg-nvidia-brand/10 text-nvidia-brand border border-nvidia-brand/20 px-1.5 py-0.5 rounded-full font-bold">0</span>522 </div>523 <div id="detection-tags-wrapper" class="detection-scroll flex-1 flex flex-col gap-1.5 pt-1 text-[10px] text-slate-500">524 <div id="detection-empty-hint" class="space-y-1.5 leading-relaxed">525 <p>Run inference to populate detected targets here โ each result will pop in one by one.</p>526 <p class="text-[9px] text-slate-600">Adjustable: Task Type ยท Categories ยท Inference Mode ยท Resize Cap ยท Temperature ยท Top P/K ยท Max Video Frames</p>527 </div>528 </div>529 </div>530 531 </div>532 </div>533 </div>534 535 </div>536 537 <!-- Full-width Decoding Trace (always visible, no nested scroll) -->538 <div id="rich-trace-section" class="glass-panel rounded-2xl p-5">539 <div id="rich-trace-log" class="text-[10px]">540 <div class="rounded-xl border border-dashed border-white/10 bg-black/30 p-6 text-center text-[10px] text-slate-500 leading-relaxed">541 <p class="text-slate-400 font-semibold mb-1">Decoding Trace</p>542 <p>Run inference to watch model tokens pop in here โ ref labels, box coords, and stats shown in full without scrolling sideways.</p>543 </div>544 </div>545 </div>546 547 </main>548 549 <!-- Gradio client connection & app runtime logic -->550 <script type="module">551 import { client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";552 553 // State variables554 let selectedMediaType = "Image";555 let activeFile = null;556 let clientInstance = null;557 558 // Cache elements559 const mediaTypeImageBtn = document.getElementById("media-type-image");560 const mediaTypeVideoBtn = document.getElementById("media-type-video");561 const videoFramesWrapper = document.getElementById("video-frames-wrapper");562 const taskTypeSelect = document.getElementById("task-type");563 const categoriesInput = document.getElementById("categories");564 const clearSearchBtn = document.getElementById("clear-search-btn");565 const inferenceModeSelect = document.getElementById("inference-mode");566 const rawPromptPreview = document.getElementById("raw-prompt-preview");567 568 // Advanced Controls Elements569 const tempSlider = document.getElementById("temp");570 const tempVal = document.getElementById("temp-val");571 const toppSlider = document.getElementById("topp");572 const toppVal = document.getElementById("topp-val");573 const topkSlider = document.getElementById("topk");574 const topkVal = document.getElementById("topk-val");575 const shortSizeInput = document.getElementById("short-size");576 const maxFramesSlider = document.getElementById("max-frames");577 const maxFramesVal = document.getElementById("frames-val");578 579 // Workspace Preview elements580 const dropZone = document.getElementById("drop-zone");581 const uploadPrompt = document.getElementById("upload-prompt");582 const previewImage = document.getElementById("preview-image");583 const previewVideo = document.getElementById("preview-video");584 const fileInput = document.getElementById("media-file-input");585 const workspaceStatus = document.getElementById("workspace-status");586 587 // Output result elements588 const outputEmpty = document.getElementById("output-empty");589 const outputImage = document.getElementById("output-image");590 const outputVideo = document.getElementById("output-video");591 592 // Overlay and run button593 const runBtn = document.getElementById("run-btn");594 const btnText = document.getElementById("btn-text");595 const btnIcon = document.getElementById("btn-icon");596 const processingOverlay = document.getElementById("processing-overlay");597 const processingStatus = document.getElementById("processing-status");598 599 // Logging & Trace elements600 const metaStatus = document.getElementById("meta-status");601 const metaTokens = document.getElementById("meta-tokens");602 const metaBoxes = document.getElementById("meta-boxes");603 const metaTps = document.getElementById("meta-tps");604 const metaBps = document.getElementById("meta-bps");605 const metaTime = document.getElementById("meta-time");606 const detectionTagsWrapper = document.getElementById("detection-tags-wrapper");607 const detectionCountBadge = document.getElementById("detection-count-badge");608 const richTraceLog = document.getElementById("rich-trace-log");609 610 const TRACE_PLACEHOLDER_HTML = `611 <div class="rounded-xl border border-dashed border-white/10 bg-black/30 p-6 text-center text-[10px] text-slate-500 leading-relaxed">612 <p class="text-slate-400 font-semibold mb-1">Decoding Trace</p>613 <p>Run inference to watch model tokens pop in here โ ref labels, box coords, and stats shown in full without scrolling sideways.</p>614 </div>`;615 616 function setTracePlaceholder() {617 richTraceLog.innerHTML = TRACE_PLACEHOLDER_HTML;618 }619 620 function setTraceProcessing() {621 richTraceLog.innerHTML = '<p class="text-slate-400 animate-pulse p-4 text-center">Building decoding trace...</p>';622 }623 624 function setTraceHtml(html) {625 richTraceLog.innerHTML = html || TRACE_PLACEHOLDER_HTML;626 }627 628 // Connect client629 async function getClient() {630 if (!clientInstance) {631 try {632 clientInstance = await client(window.location.origin);633 } catch (e) {634 console.error("Gradio Server connection failed:", e);635 alert("Could not connect to Gradio backend. Ensure the server is active.");636 }637 }638 return clientInstance;639 }640 641 // Live values updater642 function setupLiveUpdaters() {643 tempSlider.addEventListener("input", (e) => tempVal.textContent = e.target.value);644 toppSlider.addEventListener("input", (e) => toppVal.textContent = e.target.value);645 topkSlider.addEventListener("input", (e) => topkVal.textContent = e.target.value);646 maxFramesSlider.addEventListener("input", (e) => maxFramesVal.textContent = e.target.value);647 648 // Clear search categories button649 clearSearchBtn.addEventListener("click", () => {650 categoriesInput.value = "";651 categoriesInput.focus();652 triggerPromptUpdate();653 });654 655 // Trigger prompt generation updates656 const triggerPromptUpdate = () => {657 const task = taskTypeSelect.value;658 const cat = categoriesInput.value;659 rawPromptPreview.textContent = generateRawPromptText(task, cat);660 };661 taskTypeSelect.addEventListener("change", () => {662 // OCR defaults to slow (standard AR decoding) for best text accuracy663 if (taskTypeSelect.value === "OCR") {664 inferenceModeSelect.value = "slow";665 }666 triggerPromptUpdate();667 });668 categoriesInput.addEventListener("input", triggerPromptUpdate);669 670 // Run prompt builder initially671 triggerPromptUpdate();672 }673 674 // Prompt builder mirroring python logic675 function generateRawPromptText(taskType, category) {676 if (!category) category = "objects";677 const cats = category.split(",")678 .map(c => c.trim())679 .filter(c => c.length > 0)680 .join("</c>");681 682 switch (taskType) {683 case "Detection": return `Locate all the instances that matches the following description: ${cats}.`;684 case "Grounding": return `Locate all the instances that match the following description: ${cats}.`;685 case "OCR": return "Detect all the text in box format.";686 case "GUI": return `Locate the region that matches the following description: ${cats}.`;687 case "Pointing": return `Point to: ${cats}.`;688 default: return `Locate all the instances that matches the following description: ${cats}.`;689 }690 }691 692 function formatDetectionCoords(det) {693 const coords = det.coords || [];694 if (!coords.length) return "";695 const rounded = coords.map(c => Number.isFinite(c) ? Math.round(c) : c);696 return rounded.join(", ");697 }698 699 function renderDetectionTags(detections) {700 detectionTagsWrapper.innerHTML = "";701 detectionCountBadge.textContent = "0";702 detectionCountBadge.classList.remove("det-count-pop");703 704 if (!detections.length) {705 detectionTagsWrapper.innerHTML = '<p class="text-slate-500">No objects matched the given categories.</p>';706 return;707 }708 709 // Animate count badge after tags finish popping in710 const countDelay = detections.length * 80 + 120;711 setTimeout(() => {712 detectionCountBadge.textContent = detections.length;713 detectionCountBadge.classList.add("det-count-pop");714 }, countDelay);715 716 detections.forEach((det, idx) => {717 setTimeout(() => {718 const card = document.createElement("div");719 card.className = "det-tag-pop flex items-center justify-between gap-2 px-2 py-1.5 rounded-lg bg-nvidia-brand/8 border border-nvidia-brand/20 hover:border-nvidia-brand/40 transition-colors";720 card.style.animationDelay = "0s";721 722 const labelWrap = document.createElement("div");723 labelWrap.className = "flex items-center gap-1.5 min-w-0";724 725 const typeBadge = document.createElement("span");726 typeBadge.className = "shrink-0 px-1 py-0.5 rounded text-[7px] font-bold uppercase tracking-wider bg-black/40 text-nvidia-brand border border-nvidia-brand/25";727 typeBadge.textContent = det.type || "box";728 729 const label = document.createElement("span");730 label.className = "font-bold uppercase tracking-wider text-[9px] text-nvidia-brand truncate";731 label.textContent = det.frame ? `[F${det.frame}] ${det.label}` : (det.label || "object");732 733 labelWrap.appendChild(typeBadge);734 labelWrap.appendChild(label);735 736 const coords = document.createElement("span");737 coords.className = "shrink-0 font-mono text-[8px] text-slate-500";738 const coordStr = formatDetectionCoords(det);739 coords.textContent = coordStr ? `[${coordStr}]` : "";740 741 card.appendChild(labelWrap);742 card.appendChild(coords);743 detectionTagsWrapper.appendChild(card);744 detectionTagsWrapper.scrollTop = detectionTagsWrapper.scrollHeight;745 }, idx * 80);746 });747 }748 749 function resetDetectionTagsPlaceholder() {750 detectionTagsWrapper.innerHTML = `751 <div id="detection-empty-hint" class="space-y-1.5 leading-relaxed">752 <p>Run inference to populate detected targets here โ each result will pop in one by one.</p>753 <p class="text-[9px] text-slate-600">Adjustable: Task Type ยท Categories ยท Inference Mode ยท Resize Cap ยท Temperature ยท Top P/K ยท Max Video Frames</p>754 </div>`;755 detectionCountBadge.textContent = "0";756 detectionCountBadge.classList.remove("det-count-pop");757 }758 759 // Switch workspace input styles without clearing760 function setMediaType(type) {761 selectedMediaType = type;762 if (type === "Image") {763 mediaTypeImageBtn.className = "py-1.5 rounded-lg font-semibold text-[10px] transition-all bg-nvidia-brand text-black font-outfit font-black shadow shadow-nvidia-brand/10";764 mediaTypeVideoBtn.className = "py-1.5 rounded-lg font-semibold text-[10px] text-slate-400 hover:text-slate-200 transition-all";765 videoFramesWrapper.classList.add("hidden");766 videoFramesWrapper.classList.add("opacity-50");767 videoFramesWrapper.classList.add("pointer-events-none");768 maxFramesSlider.disabled = true;769 fileInput.accept = "image/*";770 workspaceStatus.textContent = activeFile ? "Image Loaded" : "No Media Loaded";771 } else {772 mediaTypeVideoBtn.className = "py-1.5 rounded-lg font-semibold text-[10px] transition-all bg-nvidia-brand text-black font-outfit font-black shadow shadow-nvidia-brand/10";773 mediaTypeImageBtn.className = "py-1.5 rounded-lg font-semibold text-[10px] text-slate-400 hover:text-slate-200 transition-all";774 videoFramesWrapper.classList.remove("hidden");775 videoFramesWrapper.classList.remove("opacity-50");776 videoFramesWrapper.classList.remove("pointer-events-none");777 maxFramesSlider.disabled = false;778 fileInput.accept = "video/*";779 workspaceStatus.textContent = activeFile ? "Video Loaded" : "No Media Loaded";780 }781 }782 783 // Reset elements784 function clearWorkspace() {785 activeFile = null;786 previewImage.src = "";787 previewImage.classList.add("hidden");788 previewVideo.src = "";789 previewVideo.classList.add("hidden");790 uploadPrompt.classList.remove("hidden");791 if (outputEmpty) outputEmpty.classList.remove("hidden");792 outputImage.src = "";793 outputImage.classList.add("hidden");794 outputVideo.src = "";795 outputVideo.classList.add("hidden");796 workspaceStatus.textContent = "Workspace Cleared";797 resetDetectionTagsPlaceholder();798 }799 800 // Drag and drop utilities801 function setupDragDrop() {802 ['dragenter', 'dragover'].forEach(eventName => {803 dropZone.addEventListener(eventName, (e) => {804 e.preventDefault();805 dropZone.classList.add('drop-zone-active');806 }, false);807 });808 809 ['dragleave', 'drop'].forEach(eventName => {810 dropZone.addEventListener(eventName, (e) => {811 e.preventDefault();812 dropZone.classList.remove('drop-zone-active');813 }, false);814 });815 816 dropZone.addEventListener('drop', (e) => {817 const dt = e.dataTransfer;818 const file = dt.files[0];819 if (file) handleFileImport(file);820 });821 822 fileInput.addEventListener('change', (e) => {823 const file = e.target.files[0];824 if (file) handleFileImport(file);825 });826 }827 828 // Display imported media829 function handleFileImport(file) {830 uploadPrompt.classList.add("hidden");831 832 if (file.type.startsWith("image/")) {833 setMediaType("Image");834 activeFile = file;835 836 const reader = new FileReader();837 reader.onload = (e) => {838 previewImage.src = e.target.result;839 previewImage.classList.remove("hidden");840 previewVideo.classList.add("hidden");841 };842 reader.readAsDataURL(file);843 workspaceStatus.textContent = `Image Loaded: ${file.name}`;844 } else if (file.type.startsWith("video/")) {845 setMediaType("Video");846 activeFile = file;847 848 previewVideo.src = URL.createObjectURL(file);849 previewVideo.classList.remove("hidden");850 previewImage.classList.add("hidden");851 workspaceStatus.textContent = `Video Loaded: ${file.name}`;852 }853 }854 855 // Initialize preloaded examples click actions856 // Utility to fetch preloaded example assets and convert to File857 async function loadExampleFromAsset(url, filename) {858 try {859 const response = await fetch(url);860 const blob = await response.blob();861 return new File([blob], filename, { type: blob.type });862 } catch (err) {863 console.error("Failed to load example asset:", err);864 return null;865 }866 }867 868 // Initialize preloaded examples click actions869 function setupExamples() {870 document.querySelectorAll(".example-card").forEach(card => {871 card.addEventListener("click", async () => {872 const type = card.getAttribute("data-type");873 const name = card.getAttribute("data-name");874 const category = card.getAttribute("data-category");875 const task = card.getAttribute("data-task");876 const mode = card.getAttribute("data-mode");877 const assetPath = card.getAttribute("data-asset"); // e.g. "assets/book.jpg"878 879 clearWorkspace();880 workspaceStatus.textContent = `Loading ${name} example...`;881 882 // Set parameters883 taskTypeSelect.value = task;884 categoriesInput.value = category;885 inferenceModeSelect.value = mode;886 887 // Trigger live prompt update888 taskTypeSelect.dispatchEvent(new Event("change"));889 890 // Setup Media type891 setMediaType(type);892 893 // Fetch asset file with robust absolute URL resolution (works in iframe)894 const ext = type === "Image" ? "jpg" : "mp4";895 const resolvedAssetUrl = new URL(assetPath, window.location.href).href;896 console.log("Fetching example from:", resolvedAssetUrl);897 const file = await loadExampleFromAsset(resolvedAssetUrl, `${name.toLowerCase()}.${ext}`);898 if (file) {899 activeFile = file;900 uploadPrompt.classList.add("hidden");901 if (type === "Image") {902 previewImage.src = URL.createObjectURL(file);903 previewImage.classList.remove("hidden");904 previewVideo.classList.add("hidden");905 workspaceStatus.textContent = `Example Image Loaded: ${name}`;906 } else {907 previewVideo.src = URL.createObjectURL(file);908 previewVideo.classList.remove("hidden");909 previewImage.classList.add("hidden");910 workspaceStatus.textContent = `Example Video Loaded: ${name}`;911 }912 } else {913 workspaceStatus.textContent = `Failed to load ${name} example`;914 }915 });916 });917 }918 919 // Execution logic920 async function executeInference() {921 if (!activeFile) {922 alert("Please upload a media file (Image or Video) or select an example first.");923 return;924 }925 926 // Set loading state927 runBtn.disabled = true;928 btnText.textContent = "โณ Queueing Request...";929 btnIcon.textContent = "๐";930 processingOverlay.classList.remove("hidden");931 processingStatus.textContent = "Waiting for Gradio queue...";932 933 // Clean outputs934 if (outputEmpty) outputEmpty.classList.add("hidden");935 outputImage.classList.add("hidden");936 outputVideo.classList.add("hidden");937 setTraceProcessing();938 metaStatus.textContent = "Processing...";939 metaStatus.className = "text-yellow-500 font-semibold";940 detectionTagsWrapper.innerHTML = '<p class="text-slate-400 animate-pulse">Processing objects in backend...</p>';941 detectionCountBadge.textContent = "0";942 detectionCountBadge.classList.remove("det-count-pop");943 944 try {945 const clientInstance = await getClient();946 if (!clientInstance) {947 throw new Error("Unable to create Gradio Client instance.");948 }949 950 // Handle file parameter wrapping using Gradio client handle_file951 const wrappedFile = activeFile ? handle_file(activeFile) : null;952 const imageFile = (selectedMediaType === "Image") ? wrappedFile : null;953 const videoFile = (selectedMediaType === "Video") ? wrappedFile : null;954 955 // Collect configuration values956 const taskType = taskTypeSelect.value;957 const category = categoriesInput.value;958 const modelMode = inferenceModeSelect.value;959 const temp = parseFloat(tempSlider.value);960 const topp = parseFloat(toppSlider.value);961 const topk = parseInt(topkSlider.value);962 const shortSize = shortSizeInput.value ? parseInt(shortSizeInput.value) : null;963 const maxVideoFrames = parseInt(maxFramesSlider.value);964 965 processingStatus.textContent = "Running Vision Model (duration-locked)...";966 967 // Execute predictions using named parameters object matching app.py signature968 const result = await clientInstance.predict("/run_inference", {969 input_type: selectedMediaType,970 image_file: imageFile,971 video_file: videoFile,972 task_type: taskType,973 category: category,974 model_mode: modelMode,975 temp: temp,976 top_p: topp,977 top_k: topk,978 short_size: shortSize,979 question_override: null,980 max_video_frames: maxVideoFrames981 });982 983 console.log("Inference complete. API outputs:", result);984 985 // Unpack result values986 const [outImageObj, outVideoObj, meta] = result.data;987 988 if (!meta.success) {989 throw new Error(meta.error || "Backend returned processing failure.");990 }991 992 // Process image result993 if (selectedMediaType === "Image" && outImageObj) {994 outputImage.src = outImageObj.url;995 outputImage.classList.remove("hidden");996 outputVideo.classList.add("hidden");997 } 998 // Process video result999 else if (selectedMediaType === "Video" && outVideoObj) {1000 outputVideo.src = outVideoObj.url;1001 outputVideo.classList.remove("hidden");1002 outputImage.classList.add("hidden");1003 }1004 1005 // Render metrics logs1006 metaStatus.textContent = "Success";1007 metaStatus.className = "text-emerald-500 font-semibold";1008 1009 const stats = meta.stats || {};1010 metaTokens.textContent = stats.num_tokens || stats.total_frames || "-";1011 metaBoxes.textContent = stats.num_boxes || stats.processed_frames || "-";1012 metaTps.textContent = stats.tps || "-";1013 metaBps.textContent = stats.bps || "-";1014 metaTime.textContent = stats.total_time_seconds ? `${stats.total_time_seconds}s` : "Optimal";1015 1016 // Render detection tags with staggered pop-in animation1017 renderDetectionTags(meta.detections || []);1018 1019 // Render decoding trace (token-by-token pop animation from previous version)1020 setTraceHtml(meta.html);1021 1022 } catch (err) {1023 console.error("Execution failed:", err);1024 metaStatus.textContent = "Error";1025 metaStatus.className = "text-red-500 font-semibold";1026 detectionTagsWrapper.innerHTML = `<span class="text-red-400">Failed: ${err.message}</span>`;1027 setTracePlaceholder();1028 alert(`Inference failed: ${err.message}`);1029 if (outputEmpty) outputEmpty.classList.remove("hidden");1030 } finally {1031 // Restore UI state1032 runBtn.disabled = false;1033 btnText.textContent = "Run Inference";1034 btnIcon.textContent = "๐ง ";1035 processingOverlay.classList.add("hidden");1036 }1037 }1038 1039 // Add event listeners on load1040 document.addEventListener("DOMContentLoaded", () => {1041 mediaTypeImageBtn.addEventListener("click", () => {1042 if (selectedMediaType !== "Image") {1043 setMediaType("Image");1044 clearWorkspace();1045 }1046 });1047 mediaTypeVideoBtn.addEventListener("click", () => {1048 if (selectedMediaType !== "Video") {1049 setMediaType("Video");1050 clearWorkspace();1051 }1052 });1053 runBtn.addEventListener("click", executeInference);1054 1055 // Bind enter key press in Categories float bar input1056 categoriesInput.addEventListener("keydown", (e) => {1057 if (e.key === "Enter") {1058 e.preventDefault();1059 executeInference();1060 }1061 });1062 1063 setupLiveUpdaters();1064 setupDragDrop();1065 setupExamples();1066 });1067 </script>1068</body>1069</html>1070 