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>OmniShotCut Pro | Shot Boundary Detection</title> 7 <link rel="preconnect" href="https://fonts.googleapis.com"> 8 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> 10
10<script src="https://unpkg.com/lucide@latest"></script>
10 11 <style> 12 :root { 13 --bg-base: #0c0c0e; 14 --bg-surface: #141417; 15 --bg-elevated: #1c1c21; 16 --border: #2a2a2f; 17 --primary: #8b5cf6; 18 --primary-muted: rgba(139, 92, 246, 0.2); 19 --text-main: #efeff1; 20 --text-dim: #94949e; 21 --accent-green: #10b981; 22 --header-height: 48px; 23 --sidebar-width: 280px; 24 --inspector-width: 320px; 25 --timeline-height: 240px; 26 } 27 28 * { 29 box-sizing: border-box; 30 margin: 0; 31 padding: 0; 32 scrollbar-width: thin; 33 scrollbar-color: var(--border) transparent; 34 } 35 36 ::-webkit-scrollbar { width: 6px; height: 6px; } 37 ::-webkit-scrollbar-track { background: transparent; } 38 ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; } 39 ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); } 40 41 body { 42 font-family: 'Inter', sans-serif; 43 background-color: var(--bg-base); 44 color: var(--text-main); 45 height: 100vh; 46 display: grid; 47 grid-template-rows: var(--header-height) 1fr var(--timeline-height); 48 grid-template-columns: var(--sidebar-width) 1fr var(--inspector-width); 49 grid-template-areas: 50 "header header header" 51 "media preview inspector" 52 "timeline timeline timeline"; 53 overflow: hidden; 54 } 55 56 /* --- Header --- */ 57 header { 58 grid-area: header; 59 background: var(--bg-surface); 60 border-bottom: 1px solid var(--border); 61 display: flex; 62 align-items: center; 63 justify-content: space-between; 64 padding: 0 1rem; 65 z-index: 100; 66 } 67 68 .brand { 69 display: flex; 70 align-items: center; 71 gap: 0.75rem; 72 font-weight: 700; 73 font-size: 0.9rem; 74 letter-spacing: -0.01em; 75 } 76 77 .brand svg { color: var(--primary); } 78 79 .header-actions { 80 display: flex; 81 align-items: center; 82 gap: 1rem; 83 } 84 85 /* --- Media Sidebar --- */ 86 .sidebar { 87 grid-area: media; 88 background: var(--bg-surface); 89 border-right: 1px solid var(--border); 90 display: flex; 91 flex-direction: column; 92 overflow: hidden; 93 } 94 95 .panel-header { 96 padding: 0.75rem 1rem; 97 font-size: 0.75rem; 98 font-weight: 600; 99 text-transform: uppercase; 100 color: var(--text-dim); 101 border-bottom: 1px solid var(--border); 102 display: flex; 103 justify-content: space-between; 104 align-items: center; 105 } 106 107 .media-content { 108 flex: 1; 109 overflow-y: auto; 110 padding: 1rem; 111 } 112 113 .upload-card { 114 border: 2px dashed var(--border); 115 border-radius: 0.5rem; 116 padding: 1.5rem 1rem; 117 text-align: center; 118 cursor: pointer; 119 transition: all 0.2s; 120 margin-bottom: 1.5rem; 121 } 122 123 .upload-card:hover { 124 border-color: var(--primary); 125 background: var(--primary-muted); 126 } 127 128 .upload-card i { margin-bottom: 0.5rem; color: var(--text-dim); } 129 .upload-card p { font-size: 0.8rem; font-weight: 500; } 130 131 .examples-list { 132 display: flex; 133 flex-direction: column; 134 gap: 0.75rem; 135 } 136 137 .example-node { 138 background: var(--bg-elevated); 139 border: 1px solid var(--border); 140 border-radius: 0.4rem; 141 overflow: hidden; 142 cursor: pointer; 143 transition: border-color 0.2s; 144 } 145 146 .example-node:hover { border-color: var(--primary); } 147 .example-node video { width: 100%; display: block; aspect-ratio: 16/9; object-fit: cover; } 148 .example-node span { display: block; padding: 0.4rem 0.6rem; font-size: 0.7rem; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 149 150 /* --- Preview Panel --- */ 151 .preview { 152 grid-area: preview; 153 background: #000; 154 display: flex; 155 align-items: center; 156 justify-content: center; 157 position: relative; 158 padding: 2rem; 159 } 160 161 .video-container { 162 width: 100%; 163 max-width: 1000px; 164 aspect-ratio: 16/9; 165 background: #050505; 166 box-shadow: 0 20px 50px rgba(0,0,0,0.5); 167 border-radius: 4px; 168 overflow: hidden; 169 position: relative; 170 } 171 172 #main-video { width: 100%; height: 100%; object-fit: contain; } 173 174 .analyze-overlay { 175 position: absolute; 176 bottom: 2rem; 177 right: 2rem; 178 } 179 180 .btn-analyze { 181 background: var(--primary); 182 color: white; 183 border: none; 184 padding: 0.6rem 1.25rem; 185 border-radius: 2rem; 186 font-weight: 600; 187 font-size: 0.85rem; 188 cursor: pointer; 189 box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4); 190 display: flex; 191 align-items: center; 192 gap: 0.5rem; 193 transition: all 0.2s; 194 } 195 196 .btn-analyze:hover:not(:disabled) { transform: scale(1.05); background: #7c3aed; } 197 .btn-analyze:disabled { opacity: 0.5;
197 cursor: not-allowed; } 198 199 /* --- Inspector Panel --- */ 200 .inspector { 201 grid-area: inspector; 202 background: var(--bg-surface); 203 border-left: 1px solid var(--border); 204 display: flex; 205 flex-direction: column; 206 overflow: hidden; 207 } 208 209 .inspector-content { 210 flex: 1; 211 overflow-y: auto; 212 padding: 0; 213 } 214 215 /* Result table override */ 216 .result-table-wrap { width: 100%; } 217 .result-table { width: 100%; border-collapse: collapse; font-size: 11px; font-family: 'JetBrains Mono', monospace; } 218 .result-table th { 219 background: var(--bg-elevated); 220 padding: 0.5rem; 221 text-align: left; 222 color: var(--text-dim); 223 font-weight: 500; 224 border-bottom: 1px solid var(--border); 225 position: sticky; top: 0; 226 } 227 .result-table td { padding: 0.5rem; border-bottom: 1px solid var(--border); color: var(--text-main); } 228 .result-table tr:hover { background: rgba(255,255,255,0.03); } 229 230 /* --- Timeline Panel --- */ 231 .timeline { 232 grid-area: timeline; 233 background: var(--bg-surface); 234 border-top: 1px solid var(--border); 235 display: flex; 236 flex-direction: column; 237 overflow: hidden; 238 } 239 240 .timeline-track { 241 flex: 1; 242 overflow-x: auto; 243 overflow-y: hidden; 244 padding: 1rem; 245 display: flex; 246 gap: 4px; 247 align-items: center; 248 background: var(--bg-base); 249 } 250 251 .timeline-item { 252 flex: 0 0 280px; 253 aspect-ratio: 16/9; 254 background: var(--bg-elevated); 255 border: 1px solid var(--border); 256 border-radius: 2px; 257 overflow: hidden; 258 position: relative; 259 cursor: pointer; 260 transition: transform 0.2s, border-color 0.2s; 261 } 262 263 .timeline-item:hover { 264 border-color: var(--primary); 265 z-index: 10; 266 } 267 268 .timeline-item img { width: 100%; height: 100%; object-fit: cover; } 269 .timeline-item .timestamp { 270 position: absolute; 271 bottom: 4px; 272 left: 4px; 273 background: rgba(0,0,0,0.7); 274 padding: 2px 4px; 275 font-size: 10px; 276 font-family: 'JetBrains Mono', monospace; 277 border-radius: 2px; 278 } 279 280 /* --- Loader --- */ 281 .loader-overlay { 282 position: fixed; 283 inset: 0; 284 background: rgba(0,0,0,0.8); 285 backdrop-filter: blur(8px); 286 z-index: 1000; 287 display: none; 288 flex-direction: column; 289 align-items: center; 290 justify-content: center; 291 } 292 293 .spinner { 294 width: 48px; 295 height: 48px; 296 border: 3px solid var(--border); 297 border-top-color: var(--primary); 298 border-radius: 50%; 299 animation: spin 1s linear infinite; 300 } 301 302 @keyframes spin { to { transform: rotate(360deg); } } 303 304 /* --- Modal --- */ 305 .modal { 306 position: fixed; 307 inset: 0; 308 background: rgba(0,0,0,0.95); 309 z-index: 2000; 310 display: none; 311 align-items: center; 312 justify-content: center; 313 padding: 2rem; 314 } 315
316 .modal-content { max-width: 90%; max-height: 90%; object-fit: contain; } 317 .close-modal { position: absolute; top: 1rem; right: 1rem; cursor: pointer; color: var(--text-dim); } 318 319 .empty-state { 320 display: flex; 321 flex-direction: column; 322 align-items: center; 323 justify-content: center; 324 height: 100%; 325 color: var(--text-dim); 326 font-size: 0.8rem; 327 text-align: center; 328 padding: 2rem; 329 } 330 </style> 331</head> 332<body> 333 334 <header> 335 <div class="brand"> 336 <i data-lucide="video"></i> 337 338 <div style="display: flex; flex-direction: column; gap: 2px;"> 339 <span>OmniShotCut: Holistic Relational Shot Boundary Detection with Shot-Query Transformer </span> 340 341 <p style="white-space: nowrap; margin: 0; display: flex; gap: 6px; align-items: center;"> 342 <a href="https://arxiv.org/abs/2604.24762" target="_blank"> 343 <img src="https://img.shields.io/badge/arXiv-Paper-b31b1b?logo=arxiv&logoColor=white" style="height: 18px;"> 344 </a> 345 <a href="https://uva-computer-vision-lab.github.io/OmniShotCut_website/" target="_blank"> 346 <img src="https://img.shields.io/badge/Project-Website-pink?logo=googlechrome&logoColor=white" style="height: 18px;"> 347 </a> 348 <a href="https://github.com/UVA-Computer-Vision-Lab/OmniShotCut" target="_blank"> 349 <img src="https://img.shields.io/badge/GitHub-Code-black?logo=github&logoColor=white" style="height: 18px;"> 350 </a> 351 </p> 352 </div> 353 </div> 354 <div class="header-actions"> 355 <span id="status-tag" style="font-size: 0.7rem; color: var(--accent-green); display: flex; align-items: center; gap: 0.4rem;"> 356 <span style="width: 6px; height: 6px; background: var(--accent-green); border-radius: 50%;"></span> 357 Engine Ready 358 </span> 359 </div> 360 </header> 361 362 <aside class="sidebar"> 363 <div class="panel-header"> 364 <span>Media Library</span> 365 <i data-lucide="plus" size="14" style="cursor: pointer;"></i> 366 </div> 367 <div class="media-content"> 368 <div class="upload-card" id="drop-zone"> 369 <i data-lucide="upload-cloud"></i> 370 <p>Import Media</p> 371 <input type="file" id="video-input" accept="video/*" style="display: none;"> 372 </div> 373 374 <div class="panel-header" style="border: none; padding-left: 0;">Examples</div> 375 <div class="examples-list" id="examples-grid"> 376 <!-- Examples load here --> 377 </div> 378 </div> 379 </aside> 380 381 <main class="preview"> 382 <div class="video-container"> 383 <video id="main-video" controls></video> 384 <div id="empty-preview" class="empty-state"> 385 <i data-lucide="film" size="48" style="margin-bottom: 1rem; opacity: 0.2;"></i> 386 <p>No media loaded.<br>Import a file to begin analysis.</p> 387 </div> 388 </div> 389 390 <div class="analyze-overlay"> 391 <button id="run-btn" class="btn-analyze" disabled> 392 <i data-lucide="zap"></i> 393 Analyze Shots 394 </button> 395 </div> 396 </main> 397 398 <aside class="inspector"> 399 <div class="panel-header"> 400 <span>Shot Inspector</span> 401 <span id="shot-count" style="background: var(--primary-muted); padding: 1px 6px; border-radius: 4px; color: var(--primary); font-size: 10px;">0 SHOTS</span> 402 </div> 403 <div class="inspector-content" id="table-container"> 404 <div class="empty-state"> 405 <p>Results will appear here after analysis.</p> 406 </div> 407 </div> 408 </aside> 409 410 <footer class="timeline"> 411 <div class="panel-header"> 412 <span>Relational Visualization Track</span> 413 <div style="display: flex; gap: 1rem;">
414 <span style="font-size: 10px; color: var(--text-dim);">Frames: <span id="frame-info">--</span></span> 415 </div> 416 </div> 417 <div class="timeline-track" id="gallery"> 418 <!-- Timeline items load here --> 419 </div> 420 </footer> 421 422 <div class="loader-overlay" id="loader"> 423 <div class="spinner"></div> 424 <p style="margin-top: 1.5rem; font-weight: 500; letter-spacing: 0.05em; font-size: 0.9rem;">PROCESSING TEMPORAL RELATIONS...</p> 425 </div> 426 427 <div class="modal" id="modal"> 428 <i data-lucide="x" class="close-modal"></i> 429 <img class="modal-content" id="modal-img"> 430 </div> 431 432
432<script type="module"> 433 import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; 434 435 // Initialize Icons 436 lucide.createIcons(); 437 438 const dropZone = document.getElementById('drop-zone'); 439 const videoInput = document.getElementById('video-input'); 440 const mainVideo = document.getElementById('main-video'); 441 const emptyPreview = document.getElementById('empty-preview'); 442 const runBtn = document.getElementById('run-btn'); 443 const loader = document.getElementById('loader'); 444 const gallery = document.getElementById('gallery'); 445 const tableContainer = document.getElementById('table-container'); 446 const shotCountBadge = document.getElementById('shot-count'); 447 const modal = document.getElementById('modal'); 448 const modalImg = document.getElementById('modal-img'); 449 const examplesGrid = document.getElementById('examples-grid'); 450 const frameInfo = document.getElementById('frame-info'); 451 452 let selectedFile = null; 453 let client = null; 454 455 async function initClient() { 456 client = await Client.connect(window.location.origin); 457 console.log("OmniShotCut Engine Online"); 458 loadExamples(); 459 } 460 initClient(); 461
462 dropZone.onclick = () => videoInput.click(); 463 videoInput.onchange = (e) => handleFile(e.target.files[0]); 464 465 function handleFile(file) { 466 if (!file) return; 467 selectedFile = file; 468 mainVideo.src = URL.createObjectURL(file); 469 mainVideo.style.display = 'block'; 470 emptyPreview.style.display = 'none'; 471 runBtn.disabled = false; 472 } 473 474 runBtn.onclick = async () => { 475 if (!selectedFile || !client) return; 476 477 loader.style.display = 'flex'; 478 479 try { 480 const result = await client.predict("/run_inference", { 481 video_file: handle_file(selectedFile) 482 }); 483 renderResults(result.data[0]); 484 } catch (err) { 485 console.error(err); 486 alert("Analysis engine error. Check console."); 487 } finally { 488 loader.style.display = 'none'; 489 } 490 }; 491 492 function renderResults(data) { 493 if (data.error) { 494 alert(data.error); 495 return; 496 } 497 498 shotCountBadge.innerText = `${data.shot_count} SHOTS`; 499 500 // Timeline rendering 501 gallery.innerHTML = ''; 502 data.gallery.forEach((file, idx) => { 503 const item = document.createElement('div'); 504 item.className = 'timeline-item'; 505 const img = document.createElement('img'); 506 img.src = file.url; 507 508 const ts = document.createElement('div'); 509 ts.className = 'timestamp'; 510 ts.innerText = `SHOT_${idx.toString().padStart(2, '0')}`; 511 512 item.appendChild(img); 513 item.appendChild(ts); 514 515 item.onclick = () => { 516 modalImg.src = file.url; 517 modal.style.display = 'flex'; 518 }; 519 gallery.appendChild(item); 520 }); 521 522 // Table rendering 523 tableContainer.innerHTML = data.table; 524 frameInfo.innerText = data.shot_count > 0 ? "Analyzed" : "--"; 525 } 526 527 async function loadExamples() { 528 try { 529 const result = await client.predict("/get_examples", {}); 530 const examples = result.data[0]; 531 examplesGrid.innerHTML = ''; 532 533 examples.forEach(ex => { 534 const node = document.createElement('div'); 535 node.className = 'example-node'; 536 537 const video = document.createElement('video'); 538 video.src = ex.url; 539 video.muted = true; 540 video.preload = "metadata"; 541 video.onloadedmetadata = () => video.currentTime = 0.1; 542 543 const label = document.createElement('span'); 544 label.innerText = ex.orig_name || 'Clip'; 545 546 node.appendChild(video); 547 node.appendChild(label); 548 549 node.onmouseenter = () => video.play(); 550 node.onmouseleave = () => { video.pause(); video.currentTime = 0.1; }; 551 552 node.onclick = async () => { 553 const response = await fetch(ex.url); 554 const blob = await response.blob(); 555 handleFile(new File([blob], ex.orig_name || 'clip.mp4', { type: 'video/mp4' })); 556 }; 557 558 examplesGrid.appendChild(node); 559 }); 560 } catch (e) { console.error("Examples load failed"); } 561 } 562 563 document.querySelector('.close-modal').onclick = () => modal.style.display = 'none'; 564 modal.onclick = (e) => { if (e.target === modal) modal.style.display = 'none'; }; 565 566 </script>
566 567</body> 568</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.