OmniShotCut / index.html
akhaliq's picture
akhaliq HF Staff
feat: implement dashboard layout and styling with updated color palette and sidebar components
0b5b849
<!DOCTYPE html>
<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>