Spaces:
Running on Zero
Running on Zero
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>OmniShotCut Pro | Shot Boundary Detection</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <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"> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <style> | |
| :root { | |
| --bg-base: #0c0c0e; | |
| --bg-surface: #141417; | |
| --bg-elevated: #1c1c21; | |
| --border: #2a2a2f; | |
| --primary: #8b5cf6; | |
| --primary-muted: rgba(139, 92, 246, 0.2); | |
| --text-main: #efeff1; | |
| --text-dim: #94949e; | |
| --accent-green: #10b981; | |
| --header-height: 48px; | |
| --sidebar-width: 280px; | |
| --inspector-width: 320px; | |
| --timeline-height: 240px; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--border) transparent; | |
| } | |
| ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-base); | |
| color: var(--text-main); | |
| height: 100vh; | |
| display: grid; | |
| grid-template-rows: var(--header-height) 1fr var(--timeline-height); | |
| grid-template-columns: var(--sidebar-width) 1fr var(--inspector-width); | |
| grid-template-areas: | |
| "header header header" | |
| "media preview inspector" | |
| "timeline timeline timeline"; | |
| overflow: hidden; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| grid-area: header; | |
| background: var(--bg-surface); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 1rem; | |
| z-index: 100; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| font-weight: 700; | |
| font-size: 0.9rem; | |
| letter-spacing: -0.01em; | |
| } | |
| .brand svg { color: var(--primary); } | |
| .header-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| /* --- Media Sidebar --- */ | |
| .sidebar { | |
| grid-area: media; | |
| background: var(--bg-surface); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .panel-header { | |
| padding: 0.75rem 1rem; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| color: var(--text-dim); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .media-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 1rem; | |
| } | |
| .upload-card { | |
| border: 2px dashed var(--border); | |
| border-radius: 0.5rem; | |
| padding: 1.5rem 1rem; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| margin-bottom: 1.5rem; | |
| } | |
| .upload-card:hover { | |
| border-color: var(--primary); | |
| background: var(--primary-muted); | |
| } | |
| .upload-card i { margin-bottom: 0.5rem; color: var(--text-dim); } | |
| .upload-card p { font-size: 0.8rem; font-weight: 500; } | |
| .examples-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .example-node { | |
| background: var(--bg-elevated); | |
| border: 1px solid var(--border); | |
| border-radius: 0.4rem; | |
| overflow: hidden; | |
| cursor: pointer; | |
| transition: border-color 0.2s; | |
| } | |
| .example-node:hover { border-color: var(--primary); } | |
| .example-node video { width: 100%; display: block; aspect-ratio: 16/9; object-fit: cover; } | |
| .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; } | |
| /* --- Preview Panel --- */ | |
| .preview { | |
| grid-area: preview; | |
| background: #000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| padding: 2rem; | |
| } | |
| .video-container { | |
| width: 100%; | |
| max-width: 1000px; | |
| aspect-ratio: 16/9; | |
| background: #050505; | |
| box-shadow: 0 20px 50px rgba(0,0,0,0.5); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| #main-video { width: 100%; height: 100%; object-fit: contain; } | |
| .analyze-overlay { | |
| position: absolute; | |
| bottom: 2rem; | |
| right: 2rem; | |
| } | |
| .btn-analyze { | |
| background: var(--primary); | |
| color: white; | |
| border: none; | |
| padding: 0.6rem 1.25rem; | |
| border-radius: 2rem; | |
| font-weight: 600; | |
| font-size: 0.85rem; | |
| cursor: pointer; | |
| box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| transition: all 0.2s; | |
| } | |
| .btn-analyze:hover:not(:disabled) { transform: scale(1.05); background: #7c3aed; } | |
| .btn-analyze:disabled { opacity: 0.5; cursor: not-allowed; } | |
| /* --- Inspector Panel --- */ | |
| .inspector { | |
| grid-area: inspector; | |
| background: var(--bg-surface); | |
| border-left: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .inspector-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 0; | |
| } | |
| /* Result table override */ | |
| .result-table-wrap { width: 100%; } | |
| .result-table { width: 100%; border-collapse: collapse; font-size: 11px; font-family: 'JetBrains Mono', monospace; } | |
| .result-table th { | |
| background: var(--bg-elevated); | |
| padding: 0.5rem; | |
| text-align: left; | |
| color: var(--text-dim); | |
| font-weight: 500; | |
| border-bottom: 1px solid var(--border); | |
| position: sticky; top: 0; | |
| } | |
| .result-table td { padding: 0.5rem; border-bottom: 1px solid var(--border); color: var(--text-main); } | |
| .result-table tr:hover { background: rgba(255,255,255,0.03); } | |
| /* --- Timeline Panel --- */ | |
| .timeline { | |
| grid-area: timeline; | |
| background: var(--bg-surface); | |
| border-top: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .timeline-track { | |
| flex: 1; | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| padding: 1rem; | |
| display: flex; | |
| gap: 4px; | |
| align-items: center; | |
| background: var(--bg-base); | |
| } | |
| .timeline-item { | |
| flex: 0 0 280px; | |
| aspect-ratio: 16/9; | |
| background: var(--bg-elevated); | |
| border: 1px solid var(--border); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| position: relative; | |
| cursor: pointer; | |
| transition: transform 0.2s, border-color 0.2s; | |
| } | |
| .timeline-item:hover { | |
| border-color: var(--primary); | |
| z-index: 10; | |
| } | |
| .timeline-item img { width: 100%; height: 100%; object-fit: cover; } | |
| .timeline-item .timestamp { | |
| position: absolute; | |
| bottom: 4px; | |
| left: 4px; | |
| background: rgba(0,0,0,0.7); | |
| padding: 2px 4px; | |
| font-size: 10px; | |
| font-family: 'JetBrains Mono', monospace; | |
| border-radius: 2px; | |
| } | |
| /* --- Loader --- */ | |
| .loader-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0,0,0,0.8); | |
| backdrop-filter: blur(8px); | |
| z-index: 1000; | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .spinner { | |
| width: 48px; | |
| height: 48px; | |
| border: 3px solid var(--border); | |
| border-top-color: var(--primary); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* --- Modal --- */ | |
| .modal { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0,0,0,0.95); | |
| z-index: 2000; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 2rem; | |
| } | |
| .modal-content { max-width: 90%; max-height: 90%; object-fit: contain; } | |
| .close-modal { position: absolute; top: 1rem; right: 1rem; cursor: pointer; color: var(--text-dim); } | |
| .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| color: var(--text-dim); | |
| font-size: 0.8rem; | |
| text-align: center; | |
| padding: 2rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i data-lucide="video"></i> | |
| <span>OMNISHOTCUT <span style="color: var(--primary)">PRO</span></span> | |
| </div> | |
| <div class="header-actions"> | |
| <span id="status-tag" style="font-size: 0.7rem; color: var(--accent-green); display: flex; align-items: center; gap: 0.4rem;"> | |
| <span style="width: 6px; height: 6px; background: var(--accent-green); border-radius: 50%;"></span> | |
| Engine Ready | |
| </span> | |
| </div> | |
| </header> | |
| <aside class="sidebar"> | |
| <div class="panel-header"> | |
| <span>Media Library</span> | |
| <i data-lucide="plus" size="14" style="cursor: pointer;"></i> | |
| </div> | |
| <div class="media-content"> | |
| <div class="upload-card" id="drop-zone"> | |
| <i data-lucide="upload-cloud"></i> | |
| <p>Import Media</p> | |
| <input type="file" id="video-input" accept="video/*" style="display: none;"> | |
| </div> | |
| <div class="panel-header" style="border: none; padding-left: 0;">Examples</div> | |
| <div class="examples-list" id="examples-grid"> | |
| <!-- Examples load here --> | |
| </div> | |
| </div> | |
| </aside> | |
| <main class="preview"> | |
| <div class="video-container"> | |
| <video id="main-video" controls></video> | |
| <div id="empty-preview" class="empty-state"> | |
| <i data-lucide="film" size="48" style="margin-bottom: 1rem; opacity: 0.2;"></i> | |
| <p>No media loaded.<br>Import a file to begin analysis.</p> | |
| </div> | |
| </div> | |
| <div class="analyze-overlay"> | |
| <button id="run-btn" class="btn-analyze" disabled> | |
| <i data-lucide="zap"></i> | |
| Analyze Shots | |
| </button> | |
| </div> | |
| </main> | |
| <aside class="inspector"> | |
| <div class="panel-header"> | |
| <span>Shot Inspector</span> | |
| <span id="shot-count" style="background: var(--primary-muted); padding: 1px 6px; border-radius: 4px; color: var(--primary); font-size: 10px;">0 SHOTS</span> | |
| </div> | |
| <div class="inspector-content" id="table-container"> | |
| <div class="empty-state"> | |
| <p>Results will appear here after analysis.</p> | |
| </div> | |
| </div> | |
| </aside> | |
| <footer class="timeline"> | |
| <div class="panel-header"> | |
| <span>Relational Visualization Track</span> | |
| <div style="display: flex; gap: 1rem;"> | |
| <span style="font-size: 10px; color: var(--text-dim);">Frames: <span id="frame-info">--</span></span> | |
| </div> | |
| </div> | |
| <div class="timeline-track" id="gallery"> | |
| <!-- Timeline items load here --> | |
| </div> | |
| </footer> | |
| <div class="loader-overlay" id="loader"> | |
| <div class="spinner"></div> | |
| <p style="margin-top: 1.5rem; font-weight: 500; letter-spacing: 0.05em; font-size: 0.9rem;">PROCESSING TEMPORAL RELATIONS...</p> | |
| </div> | |
| <div class="modal" id="modal"> | |
| <i data-lucide="x" class="close-modal"></i> | |
| <img class="modal-content" id="modal-img"> | |
| </div> | |
| <script type="module"> | |
| import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| // Initialize Icons | |
| lucide.createIcons(); | |
| const dropZone = document.getElementById('drop-zone'); | |
| const videoInput = document.getElementById('video-input'); | |
| const mainVideo = document.getElementById('main-video'); | |
| const emptyPreview = document.getElementById('empty-preview'); | |
| const runBtn = document.getElementById('run-btn'); | |
| const loader = document.getElementById('loader'); | |
| const gallery = document.getElementById('gallery'); | |
| const tableContainer = document.getElementById('table-container'); | |
| const shotCountBadge = document.getElementById('shot-count'); | |
| const modal = document.getElementById('modal'); | |
| const modalImg = document.getElementById('modal-img'); | |
| const examplesGrid = document.getElementById('examples-grid'); | |
| const frameInfo = document.getElementById('frame-info'); | |
| let selectedFile = null; | |
| let client = null; | |
| async function initClient() { | |
| client = await Client.connect(window.location.origin); | |
| console.log("OmniShotCut Engine Online"); | |
| loadExamples(); | |
| } | |
| initClient(); | |
| dropZone.onclick = () => videoInput.click(); | |
| videoInput.onchange = (e) => handleFile(e.target.files[0]); | |
| function handleFile(file) { | |
| if (!file) return; | |
| selectedFile = file; | |
| mainVideo.src = URL.createObjectURL(file); | |
| mainVideo.style.display = 'block'; | |
| emptyPreview.style.display = 'none'; | |
| runBtn.disabled = false; | |
| } | |
| runBtn.onclick = async () => { | |
| if (!selectedFile || !client) return; | |
| loader.style.display = 'flex'; | |
| try { | |
| const result = await client.predict("/run_inference", { | |
| video_file: handle_file(selectedFile) | |
| }); | |
| renderResults(result.data[0]); | |
| } catch (err) { | |
| console.error(err); | |
| alert("Analysis engine error. Check console."); | |
| } finally { | |
| loader.style.display = 'none'; | |
| } | |
| }; | |
| function renderResults(data) { | |
| if (data.error) { | |
| alert(data.error); | |
| return; | |
| } | |
| shotCountBadge.innerText = `${data.shot_count} SHOTS`; | |
| // Timeline rendering | |
| gallery.innerHTML = ''; | |
| data.gallery.forEach((file, idx) => { | |
| const item = document.createElement('div'); | |
| item.className = 'timeline-item'; | |
| const img = document.createElement('img'); | |
| img.src = file.url; | |
| const ts = document.createElement('div'); | |
| ts.className = 'timestamp'; | |
| ts.innerText = `SHOT_${idx.toString().padStart(2, '0')}`; | |
| item.appendChild(img); | |
| item.appendChild(ts); | |
| item.onclick = () => { | |
| modalImg.src = file.url; | |
| modal.style.display = 'flex'; | |
| }; | |
| gallery.appendChild(item); | |
| }); | |
| // Table rendering | |
| tableContainer.innerHTML = data.table; | |
| frameInfo.innerText = data.shot_count > 0 ? "Analyzed" : "--"; | |
| } | |
| async function loadExamples() { | |
| try { | |
| const result = await client.predict("/get_examples", {}); | |
| const examples = result.data[0]; | |
| examplesGrid.innerHTML = ''; | |
| examples.forEach(ex => { | |
| const node = document.createElement('div'); | |
| node.className = 'example-node'; | |
| const video = document.createElement('video'); | |
| video.src = ex.url; | |
| video.muted = true; | |
| video.preload = "metadata"; | |
| video.onloadedmetadata = () => video.currentTime = 0.1; | |
| const label = document.createElement('span'); | |
| label.innerText = ex.orig_name || 'Clip'; | |
| node.appendChild(video); | |
| node.appendChild(label); | |
| node.onmouseenter = () => video.play(); | |
| node.onmouseleave = () => { video.pause(); video.currentTime = 0.1; }; | |
| node.onclick = async () => { | |
| const response = await fetch(ex.url); | |
| const blob = await response.blob(); | |
| handleFile(new File([blob], ex.orig_name || 'clip.mp4', { type: 'video/mp4' })); | |
| }; | |
| examplesGrid.appendChild(node); | |
| }); | |
| } catch (e) { console.error("Examples load failed"); } | |
| } | |
| document.querySelector('.close-modal').onclick = () => modal.style.display = 'none'; | |
| modal.onclick = (e) => { if (e.target === modal) modal.style.display = 'none'; }; | |
| </script> | |
| </body> | |
| </html> | |