Spaces:
Running
Running
| // assets/js/video_segment.js | |
| const DEFAULT_FPS = 30; | |
| /** | |
| * Playback rules by label (case-insensitive): | |
| * | |
| * time : video start -> endFrame | |
| * success : entire video | |
| * failure : entire video | |
| * safety conflict : startFrame -> endFrame | |
| * safety avoidance : startFrame -> end of video | |
| * passive wait : startFrame -> endFrame | |
| * redundant retrieval : startFrame -> end of video | |
| * task model uncertainty : startFrame -> end of video | |
| * capability miscalibration : startFrame -> endFrame | |
| * missed grab : (startFrame-10) -> (endFrame+50 or end of video) | |
| * slippage : (startFrame-10) -> (endFrame+50 or end of video) | |
| * | |
| * Manual control requirements: | |
| * 1) If user pauses, DO NOT auto-resume. | |
| * 2) If user seeks/scrubs anywhere, allow it. When playback reaches the window end, | |
| * it loops back to the window start (and continues only if user is playing). | |
| */ | |
| function normalizeLabel(label) { | |
| return String(label || "").trim().toLowerCase(); | |
| } | |
| function computeWindowFrames(label, startFrame, endFrame) { | |
| const lab = normalizeLabel(label); | |
| let s = Number(startFrame); | |
| let e = Number(endFrame); | |
| if (!Number.isFinite(s)) s = 0; | |
| if (!Number.isFinite(e)) e = 0; | |
| if (s > e) [s, e] = [e, s]; | |
| let start = s; | |
| let end = e; | |
| let endIsVideo = false; | |
| const isTime = lab === "time"; | |
| const isSuccess = lab === "success"; | |
| const isFailure = lab === "failure"; | |
| const isSafetyConflict = lab === "safety conflict"; | |
| const isSafetyAvoidance = lab === "safety avoidance"; | |
| const isPassiveWait = lab === "passive wait"; | |
| const isRedundantRetrieval = lab === "redundant retrieval"; | |
| const isTMU = lab === "task model uncertainty"; | |
| const isCapMis = lab === "capability miscalibration"; | |
| const isMissedGrab = lab === "missed grab"; | |
| const isSlippage = lab === "slippage"; | |
| if (isTime) { | |
| start = 0; | |
| end = e; | |
| endIsVideo = false; | |
| } else if (isSuccess || isFailure) { | |
| start = 0; | |
| endIsVideo = true; | |
| } else if (isSafetyConflict || isPassiveWait || isCapMis) { | |
| start = s; | |
| end = e; | |
| endIsVideo = false; | |
| } else if (isSafetyAvoidance || isRedundantRetrieval || isTMU) { | |
| start = s; | |
| endIsVideo = true; | |
| } else if (isMissedGrab || isSlippage) { | |
| start = Math.max(0, s - 10); | |
| end = e + 50; // will clamp to duration | |
| endIsVideo = false; | |
| } else { | |
| // Unknown label: just use annotated segment | |
| start = s; | |
| end = e; | |
| endIsVideo = false; | |
| } | |
| start = Math.max(0, Math.floor(start)); | |
| if (!endIsVideo) end = Math.max(start, Math.floor(end)); | |
| return { startFrameAdj: start, endFrameAdj: end, endIsVideo }; | |
| } | |
| /** | |
| * Attaches labeled looping to a <video>, while preserving manual user control. | |
| * | |
| * - Autoplays (muted) so the page is lively. | |
| * - If user pauses, we do NOT force play(). | |
| * - If user seeks anywhere, we allow it. | |
| * - When playback crosses the computed end time, we jump back to the start time. | |
| * If the video was playing, we keep playing. If paused, it stays paused. | |
| */ | |
| function attachLabeledLoop(videoEl, { label, startFrame, endFrame, fps = DEFAULT_FPS }) { | |
| const { startFrameAdj, endFrameAdj, endIsVideo } = computeWindowFrames(label, startFrame, endFrame); | |
| const startSec = Math.max(0, startFrameAdj / fps); | |
| let endSec = Math.max(startSec, endFrameAdj / fps); | |
| // Autoplay compatibility: muted + playsInline | |
| videoEl.muted = true; | |
| videoEl.playsInline = true; | |
| // Track whether a pause was user-initiated (so we never override it) | |
| let userPaused = false; | |
| function safePlay() { | |
| const p = videoEl.play(); | |
| if (p && typeof p.catch === "function") p.catch(() => { }); | |
| } | |
| function jumpToStart(keepPaused) { | |
| try { | |
| videoEl.currentTime = startSec; | |
| } catch (_) { } | |
| if (!keepPaused) safePlay(); | |
| } | |
| // Setup final endSec based on duration once known | |
| videoEl.addEventListener("loadedmetadata", () => { | |
| const dur = Number.isFinite(videoEl.duration) ? videoEl.duration : null; | |
| if (dur != null) { | |
| if (endIsVideo) { | |
| endSec = dur; | |
| } else if (endSec > dur) { | |
| endSec = dur; | |
| } | |
| } | |
| // Start at window start and autoplay (muted) | |
| userPaused = false; | |
| jumpToStart(false); | |
| }); | |
| // If user pauses, respect it | |
| videoEl.addEventListener("pause", () => { | |
| // If it paused because it reached the natural end (rare), userPaused still ok | |
| userPaused = true; | |
| }); | |
| // If user hits play, allow it (resume normal looping) | |
| videoEl.addEventListener("play", () => { | |
| userPaused = false; | |
| }); | |
| // Core loop logic: when reaching endSec, loop back to startSec | |
| videoEl.addEventListener("timeupdate", () => { | |
| // epsilon to prevent jitter | |
| if (endSec <= startSec + 0.01) return; | |
| if (videoEl.currentTime >= endSec - 0.02) { | |
| // Loop back, but DO NOT resume if user paused | |
| const keepPaused = videoEl.paused || userPaused; | |
| jumpToStart(keepPaused); | |
| } | |
| }); | |
| // IMPORTANT CHANGE: allow free seeking/scrubbing. | |
| // No snapping during seek; we only enforce the window when time reaches endSec. | |
| } | |
| window.DEFAULT_FPS = DEFAULT_FPS; | |
| window.attachLabeledLoop = attachLabeledLoop; | |
| window.computeWindowFrames = computeWindowFrames; | |