| |
|
|
| function el(id) { |
| const node = document.getElementById(id); |
| if (!node) throw new Error(`Missing element: ${id}`); |
| return node; |
| } |
|
|
| function getSearchParams() { |
| return new URLSearchParams(window.location.search); |
| } |
|
|
| function getQueryValue(name) { |
| return getSearchParams().get(name); |
| } |
|
|
| function titleCase(text) { |
| return String(text || "") |
| .split(/[-_\s]+/) |
| .filter(Boolean) |
| .map((part) => part[0].toUpperCase() + part.slice(1)) |
| .join(" "); |
| } |
|
|
| function clampRate(value) { |
| const numeric = Number(value); |
| if (!Number.isFinite(numeric) || numeric <= 0) return 1; |
| return numeric; |
| } |
|
|
| function buildAssetUrl(mode, manifest, type, path) { |
| if (mode === "hub") { |
| const repo = |
| type === "gt" ? manifest.repos.gt_dataset : manifest.repos.generated_dataset; |
| return `https://huggingface.co/datasets/${repo}/resolve/main/${path}`; |
| } |
| if (type === "gt") { |
| return `/data/${path}`; |
| } |
| return `/data/sub_generated_videos/${path}`; |
| } |
|
|
| function isLocalPreviewHost() { |
| const host = window.location.hostname || ""; |
| return host === "localhost" || host === "127.0.0.1"; |
| } |
|
|
| function detectSourceMode() { |
| const explicit = getQueryValue("source"); |
| if (explicit === "hub") return "hub"; |
| if (explicit === "local" && isLocalPreviewHost()) return "local"; |
| if (isLocalPreviewHost()) return "local"; |
| return "hub"; |
| } |
|
|
| function sameValue(a, b, epsilon) { |
| return Math.abs(a - b) <= epsilon; |
| } |
|
|
| function setVideoSource(video, src) { |
| if (!src) { |
| video.removeAttribute("src"); |
| video.load(); |
| return; |
| } |
| if (video.dataset.currentSrc === src) return; |
| video.dataset.currentSrc = src; |
| video.src = src; |
| video.load(); |
| } |
|
|
| function createButton(label, active, onClick, className) { |
| const button = document.createElement("button"); |
| button.type = "button"; |
| button.className = className; |
| if (active) button.classList.add("active"); |
| button.textContent = label; |
| button.addEventListener("click", onClick); |
| return button; |
| } |
|
|
| function createResultCard(result) { |
| const article = document.createElement("article"); |
| article.className = "video-card result-card"; |
| article.dataset.engine = result.engine; |
| article.dataset.model = result.model; |
|
|
| const header = document.createElement("div"); |
| header.className = "card-header"; |
|
|
| const badges = document.createElement("div"); |
| badges.className = "badge-stack"; |
|
|
| const engineBadge = document.createElement("span"); |
| engineBadge.className = "badge"; |
| engineBadge.textContent = result.engine_label; |
| badges.appendChild(engineBadge); |
|
|
| const modelBadge = document.createElement("span"); |
| modelBadge.className = "badge badge-secondary"; |
| modelBadge.textContent = result.model_label; |
| badges.appendChild(modelBadge); |
|
|
| const title = document.createElement("div"); |
| title.className = "card-title"; |
| title.textContent = result.model_label; |
|
|
| const subtitle = document.createElement("div"); |
| subtitle.className = "card-subtitle"; |
| subtitle.textContent = `${result.engine_label} renderer`; |
|
|
| const video = document.createElement("video"); |
| video.controls = true; |
| video.preload = "metadata"; |
| video.playsInline = true; |
| video.dataset.role = "result-video"; |
|
|
| header.appendChild(badges); |
| header.appendChild(title); |
| header.appendChild(subtitle); |
|
|
| article.appendChild(header); |
| article.appendChild(video); |
|
|
| return { article, video }; |
| } |
|
|
| function parseSetParam(name, validValues) { |
| const raw = getQueryValue(name); |
| if (!raw) return null; |
| const out = new Set(); |
| for (const part of raw.split(",")) { |
| const item = decodeURIComponent(part).trim(); |
| if (item && validValues.has(item)) out.add(item); |
| } |
| return out.size > 0 ? out : null; |
| } |
|
|
| function setsEqual(left, right) { |
| if (left.size !== right.size) return false; |
| for (const item of left) { |
| if (!right.has(item)) return false; |
| } |
| return true; |
| } |
|
|
| async function loadManifest() { |
| const response = await fetch("./static/manifest.json", { cache: "no-store" }); |
| if (!response.ok) throw new Error(`Failed to load manifest: ${response.status}`); |
| return response.json(); |
| } |
|
|
| async function copyToClipboard(text) { |
| if (navigator.clipboard && navigator.clipboard.writeText) { |
| await navigator.clipboard.writeText(text); |
| return true; |
| } |
| return false; |
| } |
|
|
| async function main() { |
| const manifest = await loadManifest(); |
| const samples = Array.isArray(manifest.samples) ? manifest.samples : []; |
| if (samples.length === 0) throw new Error("Manifest has no samples"); |
|
|
| const allModels = []; |
| const seenModels = new Set(); |
| for (const engine of manifest.engine_order) { |
| for (const model of manifest.model_order[engine] || []) { |
| if (seenModels.has(model)) continue; |
| seenModels.add(model); |
| allModels.push(model); |
| } |
| } |
|
|
| const engineSet = new Set(manifest.engine_order); |
| const modelSet = new Set(allModels); |
|
|
| const state = { |
| samples, |
| sourceMode: detectSourceMode(), |
| currentIndex: 0, |
| syncEnabled: getQueryValue("sync") !== "0", |
| muted: getQueryValue("muted") !== "0", |
| propagating: false, |
| selectedEngines: parseSetParam("engines", engineSet) || new Set(manifest.engine_order), |
| selectedModels: parseSetParam("models", modelSet) || new Set(allModels), |
| playbackRate: clampRate(getQueryValue("rate") || "1"), |
| }; |
|
|
| const sampleSelect = el("sample-select"); |
| const sampleSearch = el("sample-search"); |
| const sampleCounter = el("sample-counter"); |
| const sourceLocal = el("source-local"); |
| const sourceHub = el("source-hub"); |
| const sourceNote = el("source-note"); |
|
|
| const metaTask = el("meta-task"); |
| const metaSplit = el("meta-split"); |
| const metaResults = el("meta-results"); |
| const metaDetection = el("meta-detection"); |
| const sampleMeta = el("sample-meta"); |
|
|
| const gtVideo = el("gt-video"); |
| const gtTitle = el("gt-title"); |
| const comparisonTitle = el("comparison-title"); |
| const resultsSummary = el("results-summary"); |
| const activeFilterSummary = el("active-filter-summary"); |
| const resultsGrid = el("results-grid"); |
| const gtCard = resultsGrid.querySelector(".gt-card"); |
| const emptyState = el("empty-state"); |
|
|
| const engineFilters = el("engine-filters"); |
| const modelFilters = el("model-filters"); |
| const presetBar = el("preset-bar"); |
|
|
| const syncToggle = el("sync-toggle"); |
| const muteToggle = el("mute-toggle"); |
| const playbackRate = el("playback-rate"); |
| const copyLinkButton = el("copy-link"); |
|
|
| playbackRate.value = String(state.playbackRate); |
|
|
| const presetDefinitions = [ |
| { |
| key: "all", |
| label: "All", |
| engines: manifest.engine_order, |
| models: allModels, |
| }, |
| { |
| key: "code", |
| label: "Code", |
| engines: ["threejs", "p5js"], |
| models: allModels.filter((model) => model !== "svd-img2vid" && model !== "sora-2" && model !== "veo31"), |
| }, |
| { |
| key: "threejs", |
| label: "Three.js", |
| engines: ["threejs"], |
| models: manifest.model_order.threejs || [], |
| }, |
| { |
| key: "p5js", |
| label: "p5.js", |
| engines: ["p5js"], |
| models: manifest.model_order.p5js || [], |
| }, |
| { |
| key: "video", |
| label: "Video", |
| engines: ["video"], |
| models: manifest.model_order.video || [], |
| }, |
| ]; |
|
|
| function currentSample() { |
| return state.samples[state.currentIndex]; |
| } |
|
|
| function allVisibleVideos() { |
| return Array.from(resultsGrid.querySelectorAll("video")); |
| } |
|
|
| function updateUrlState() { |
| const params = new URLSearchParams(); |
| params.set("sample", currentSample().id); |
| params.set("source", state.sourceMode); |
| if (!setsEqual(state.selectedEngines, new Set(manifest.engine_order))) { |
| params.set("engines", Array.from(state.selectedEngines).join(",")); |
| } |
| if (!setsEqual(state.selectedModels, new Set(allModels))) { |
| params.set("models", Array.from(state.selectedModels).join(",")); |
| } |
| if (!state.syncEnabled) params.set("sync", "0"); |
| if (!state.muted) params.set("muted", "0"); |
| if (!sameValue(state.playbackRate, 1, 0.001)) { |
| params.set("rate", String(state.playbackRate)); |
| } |
| const nextUrl = `${window.location.pathname}?${params.toString()}`; |
| window.history.replaceState(null, "", nextUrl); |
| } |
|
|
| function updateSourceButtons() { |
| const localPreview = isLocalPreviewHost(); |
| sourceLocal.disabled = !localPreview; |
| sourceLocal.title = localPreview ? "Read local files under /data" : "Local mode is only available in local preview"; |
| sourceLocal.classList.toggle("active", state.sourceMode === "local"); |
| sourceHub.classList.toggle("active", state.sourceMode === "hub"); |
| sourceNote.textContent = |
| state.sourceMode === "local" |
| ? "Reading videos from this repo under /data. This is the best mode for local inspection." |
| : localPreview |
| ? "Reading videos from the public Hugging Face datasets. This matches the deployed Space." |
| : "Reading videos from the public Hugging Face datasets used by this Space."; |
| } |
|
|
| function updateSyncButtons() { |
| syncToggle.textContent = `Sync: ${state.syncEnabled ? "On" : "Off"}`; |
| muteToggle.textContent = `Muted: ${state.muted ? "On" : "Off"}`; |
| syncToggle.classList.toggle("active", state.syncEnabled); |
| muteToggle.classList.toggle("active", state.muted); |
| } |
|
|
| function updateCounter() { |
| sampleCounter.textContent = `${state.currentIndex + 1}/${state.samples.length}`; |
| } |
|
|
| function applyPlaybackSettings() { |
| state.playbackRate = clampRate(playbackRate.value || "1"); |
| for (const video of allVisibleVideos()) { |
| video.playbackRate = state.playbackRate; |
| video.defaultPlaybackRate = state.playbackRate; |
| video.muted = state.muted; |
| } |
| } |
|
|
| function propagate(origin, action) { |
| if (!state.syncEnabled || state.propagating) return; |
| state.propagating = true; |
| const videos = allVisibleVideos(); |
|
|
| try { |
| if (action === "play") { |
| for (const video of videos) { |
| if (video === origin) continue; |
| if (!sameValue(video.currentTime, origin.currentTime, 0.08)) { |
| video.currentTime = origin.currentTime; |
| } |
| video.playbackRate = origin.playbackRate; |
| video.play().catch(() => { |
| |
| }); |
| } |
| } else if (action === "pause") { |
| for (const video of videos) { |
| if (video === origin) continue; |
| if (!sameValue(video.currentTime, origin.currentTime, 0.08)) { |
| video.currentTime = origin.currentTime; |
| } |
| video.pause(); |
| } |
| } else if (action === "seek") { |
| for (const video of videos) { |
| if (video === origin) continue; |
| if (!sameValue(video.currentTime, origin.currentTime, 0.1)) { |
| video.currentTime = origin.currentTime; |
| } |
| } |
| } else if (action === "rate") { |
| for (const video of videos) { |
| if (video === origin) continue; |
| video.playbackRate = origin.playbackRate; |
| video.defaultPlaybackRate = origin.playbackRate; |
| } |
| } |
| } finally { |
| window.setTimeout(() => { |
| state.propagating = false; |
| }, 0); |
| } |
| } |
|
|
| function bindSync(video) { |
| video.addEventListener("play", () => propagate(video, "play")); |
| video.addEventListener("pause", () => propagate(video, "pause")); |
| video.addEventListener("seeked", () => propagate(video, "seek")); |
| video.addEventListener("ratechange", () => propagate(video, "rate")); |
| } |
|
|
| function modelsForEngines(engines) { |
| const out = new Set(); |
| for (const engine of engines) { |
| for (const model of manifest.model_order[engine] || []) { |
| out.add(model); |
| } |
| } |
| return out; |
| } |
|
|
| function activePresetKey() { |
| for (const preset of presetDefinitions) { |
| if ( |
| setsEqual(state.selectedEngines, new Set(preset.engines)) && |
| setsEqual(state.selectedModels, new Set(preset.models)) |
| ) { |
| return preset.key; |
| } |
| } |
| return null; |
| } |
|
|
| function describeFilters() { |
| const engineSummary = |
| state.selectedEngines.size === manifest.engine_order.length |
| ? "All engines" |
| : `${state.selectedEngines.size} engine${state.selectedEngines.size === 1 ? "" : "s"}`; |
| const modelSummary = |
| state.selectedModels.size === allModels.length |
| ? "all models" |
| : `${state.selectedModels.size} model${state.selectedModels.size === 1 ? "" : "s"}`; |
| return `${engineSummary} · ${modelSummary}`; |
| } |
|
|
| function setFilterState(engines, models) { |
| state.selectedEngines = new Set(engines); |
| state.selectedModels = new Set(models); |
| renderFilterButtons(); |
| renderSample(); |
| } |
|
|
| function renderFilterButtons() { |
| const activePreset = activePresetKey(); |
|
|
| presetBar.innerHTML = ""; |
| for (const preset of presetDefinitions) { |
| const button = createButton( |
| preset.label, |
| activePreset === preset.key, |
| () => setFilterState(preset.engines, preset.models), |
| "preset-btn" |
| ); |
| presetBar.appendChild(button); |
| } |
|
|
| engineFilters.innerHTML = ""; |
| for (const engine of manifest.engine_order) { |
| const button = createButton( |
| manifest.engine_labels[engine] || titleCase(engine), |
| state.selectedEngines.has(engine), |
| () => { |
| if (state.selectedEngines.has(engine)) state.selectedEngines.delete(engine); |
| else state.selectedEngines.add(engine); |
| if (state.selectedEngines.size === 0) state.selectedEngines.add(engine); |
| renderFilterButtons(); |
| renderSample(); |
| }, |
| "pill-btn" |
| ); |
| engineFilters.appendChild(button); |
| } |
|
|
| modelFilters.innerHTML = ""; |
| for (const model of allModels) { |
| const button = createButton( |
| manifest.model_labels[model] || titleCase(model), |
| state.selectedModels.has(model), |
| () => { |
| if (state.selectedModels.has(model)) state.selectedModels.delete(model); |
| else state.selectedModels.add(model); |
| if (state.selectedModels.size === 0) state.selectedModels.add(model); |
| renderFilterButtons(); |
| renderSample(); |
| }, |
| "pill-btn" |
| ); |
| modelFilters.appendChild(button); |
| } |
| } |
|
|
| function renderSampleOptions(filterText) { |
| const normalized = String(filterText || "").trim().toLowerCase(); |
| sampleSelect.innerHTML = ""; |
| let nextSelectedValue = null; |
|
|
| for (const sample of state.samples) { |
| const haystack = `${sample.id} ${sample.label} ${sample.difficulty} ${ |
| sample.is_3d ? "3d" : "2d" |
| }`.toLowerCase(); |
| if (normalized && !haystack.includes(normalized)) continue; |
|
|
| const option = document.createElement("option"); |
| option.value = sample.id; |
| option.textContent = sample.label; |
| sampleSelect.appendChild(option); |
| if (sample.id === currentSample().id) nextSelectedValue = sample.id; |
| } |
|
|
| if (nextSelectedValue) { |
| sampleSelect.value = nextSelectedValue; |
| } else if (sampleSelect.options.length > 0) { |
| sampleSelect.selectedIndex = 0; |
| } |
| } |
|
|
| function setSampleByIndex(index) { |
| state.currentIndex = Math.max(0, Math.min(state.samples.length - 1, index)); |
| sampleSelect.value = currentSample().id; |
| updateCounter(); |
| renderSample(); |
| } |
|
|
| function renderSampleMeta(sample) { |
| sampleMeta.innerHTML = ""; |
|
|
| const chips = [ |
| { label: sample.is_3d ? "3D" : "2D", tone: "dimension" }, |
| { label: sample.difficulty || "unknown", tone: sample.difficulty || "default" }, |
| { label: `${sample.available_result_count} results`, tone: "count" }, |
| ]; |
|
|
| for (const chip of chips) { |
| const span = document.createElement("span"); |
| span.className = `meta-chip ${chip.tone}`; |
| span.textContent = chip.label; |
| sampleMeta.appendChild(span); |
| } |
|
|
| metaTask.textContent = sample.id; |
| metaSplit.textContent = sample.split || "sub"; |
| metaResults.textContent = `${sample.available_result_count}`; |
| metaDetection.textContent = sample.detection_json_path ? "available" : "none"; |
| } |
|
|
| function filteredResults(sample) { |
| return (sample.results || []).filter( |
| (result) => |
| state.selectedEngines.has(result.engine) && |
| state.selectedModels.has(result.model) |
| ); |
| } |
|
|
| function renderSample() { |
| const sample = currentSample(); |
| const visibleResults = filteredResults(sample); |
|
|
| sampleSelect.value = sample.id; |
| comparisonTitle.textContent = `${sample.id} · ${sample.is_3d ? "3D" : "2D"} · ${titleCase(sample.difficulty)}`; |
| gtTitle.textContent = `${sample.id} · Ground Truth`; |
| activeFilterSummary.textContent = describeFilters(); |
| renderSampleMeta(sample); |
|
|
| setVideoSource(gtVideo, buildAssetUrl(state.sourceMode, manifest, "gt", sample.gt.path)); |
|
|
| for (const card of Array.from(resultsGrid.querySelectorAll(".result-card"))) { |
| card.remove(); |
| } |
|
|
| for (const result of visibleResults) { |
| const card = createResultCard(result); |
| setVideoSource(card.video, buildAssetUrl(state.sourceMode, manifest, "generated", result.path)); |
| card.video.muted = state.muted; |
| card.video.playbackRate = state.playbackRate; |
| bindSync(card.video); |
| resultsGrid.appendChild(card.article); |
| } |
|
|
| if (gtCard && resultsGrid.firstElementChild !== gtCard) { |
| resultsGrid.insertBefore(gtCard, resultsGrid.firstElementChild); |
| } |
|
|
| resultsSummary.textContent = `${visibleResults.length} generated · ${sample.available_result_count} total`; |
| emptyState.hidden = visibleResults.length > 0; |
| applyPlaybackSettings(); |
| updateUrlState(); |
| } |
|
|
| bindSync(gtVideo); |
|
|
| for (const sample of state.samples) { |
| const option = document.createElement("option"); |
| option.value = sample.id; |
| option.textContent = sample.label; |
| sampleSelect.appendChild(option); |
| } |
|
|
| const requestedSample = getQueryValue("sample"); |
| if (requestedSample) { |
| const requestedIndex = state.samples.findIndex((sample) => sample.id === requestedSample); |
| if (requestedIndex >= 0) { |
| state.currentIndex = requestedIndex; |
| } |
| } |
|
|
| sampleSelect.addEventListener("change", () => { |
| const nextId = sampleSelect.value; |
| const nextIndex = state.samples.findIndex((sample) => sample.id === nextId); |
| if (nextIndex >= 0) setSampleByIndex(nextIndex); |
| }); |
|
|
| sampleSearch.addEventListener("input", () => { |
| renderSampleOptions(sampleSearch.value); |
| }); |
|
|
| sampleSearch.addEventListener("keydown", (event) => { |
| if (event.key !== "Enter") return; |
| const nextId = sampleSelect.value; |
| const nextIndex = state.samples.findIndex((sample) => sample.id === nextId); |
| if (nextIndex >= 0) setSampleByIndex(nextIndex); |
| }); |
|
|
| el("prev-sample").addEventListener("click", () => setSampleByIndex(state.currentIndex - 1)); |
| el("next-sample").addEventListener("click", () => setSampleByIndex(state.currentIndex + 1)); |
|
|
| sourceLocal.addEventListener("click", () => { |
| if (!isLocalPreviewHost()) return; |
| state.sourceMode = "local"; |
| updateSourceButtons(); |
| renderSample(); |
| }); |
|
|
| sourceHub.addEventListener("click", () => { |
| state.sourceMode = "hub"; |
| updateSourceButtons(); |
| renderSample(); |
| }); |
|
|
| syncToggle.addEventListener("click", () => { |
| state.syncEnabled = !state.syncEnabled; |
| updateSyncButtons(); |
| updateUrlState(); |
| }); |
|
|
| muteToggle.addEventListener("click", () => { |
| state.muted = !state.muted; |
| updateSyncButtons(); |
| applyPlaybackSettings(); |
| updateUrlState(); |
| }); |
|
|
| playbackRate.addEventListener("change", () => { |
| applyPlaybackSettings(); |
| updateUrlState(); |
| }); |
|
|
| el("play-all").addEventListener("click", () => { |
| for (const video of allVisibleVideos()) { |
| video.play().catch(() => { |
| |
| }); |
| } |
| }); |
|
|
| el("pause-all").addEventListener("click", () => { |
| for (const video of allVisibleVideos()) { |
| video.pause(); |
| } |
| }); |
|
|
| el("restart-all").addEventListener("click", () => { |
| for (const video of allVisibleVideos()) { |
| video.currentTime = 0; |
| video.pause(); |
| } |
| }); |
|
|
| el("reset-filters").addEventListener("click", () => { |
| setFilterState(manifest.engine_order, allModels); |
| }); |
|
|
| el("random-sample").addEventListener("click", () => { |
| const nextIndex = Math.floor(Math.random() * state.samples.length); |
| setSampleByIndex(nextIndex); |
| }); |
|
|
| copyLinkButton.addEventListener("click", async () => { |
| const copied = await copyToClipboard(window.location.href).catch(() => false); |
| const original = "Copy link"; |
| copyLinkButton.textContent = copied ? "Copied" : "Copy failed"; |
| window.setTimeout(() => { |
| copyLinkButton.textContent = original; |
| }, 1200); |
| }); |
|
|
| document.addEventListener("keydown", (event) => { |
| if (event.key === "ArrowLeft") setSampleByIndex(state.currentIndex - 1); |
| if (event.key === "ArrowRight") setSampleByIndex(state.currentIndex + 1); |
| }); |
|
|
| renderSampleOptions(""); |
| renderFilterButtons(); |
| updateSourceButtons(); |
| updateSyncButtons(); |
| updateCounter(); |
| renderSample(); |
| } |
|
|
| window.addEventListener("DOMContentLoaded", () => { |
| main().catch((error) => { |
| console.error(error); |
| document.body.innerHTML = `<pre style="padding:24px;font:14px/1.5 monospace;">${String( |
| error && error.stack ? error.stack : error |
| )}</pre>`; |
| }); |
| }); |
|
|