Spaces:
Sleeping
Sleeping
Zhen Ye Claude Opus 4.6 commited on
Commit ·
58bc91d
1
Parent(s): 83e3e44
fix: use actual video FPS for frame index calculation to prevent track card disappearance
Browse filesFrontend hardcoded 30fps for frame index calculation, causing drift on non-30fps
videos. Near the end of a 24fps video, the frontend would request frame indices
beyond what the backend stored, resulting in empty track data and disappearing
cards. Now uses the actual FPS from the backend summary endpoint, with clamping
to prevent overshoot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- frontend/js/core/state.js +2 -0
- frontend/js/core/timeline.js +3 -1
- frontend/js/core/video.js +2 -0
- frontend/js/main.js +5 -1
frontend/js/core/state.js
CHANGED
|
@@ -24,6 +24,8 @@ APP.core.state = {
|
|
| 24 |
depthVideoUrl: null, // Depth video URL
|
| 25 |
depthBlob: null, // Depth video blob
|
| 26 |
summary: null,
|
|
|
|
|
|
|
| 27 |
busy: false,
|
| 28 |
lastError: null,
|
| 29 |
missionSpec: null
|
|
|
|
| 24 |
depthVideoUrl: null, // Depth video URL
|
| 25 |
depthBlob: null, // Depth video blob
|
| 26 |
summary: null,
|
| 27 |
+
fps: null, // actual video FPS from backend summary
|
| 28 |
+
totalFrames: null, // actual total frame count from backend
|
| 29 |
busy: false,
|
| 30 |
lastError: null,
|
| 31 |
missionSpec: null
|
frontend/js/core/timeline.js
CHANGED
|
@@ -60,7 +60,7 @@ APP.core.timeline = {};
|
|
| 60 |
const ctx = canvas.getContext("2d");
|
| 61 |
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
| 62 |
|
| 63 |
-
const fps = 30;
|
| 64 |
const totalFrames = Math.ceil(duration * fps);
|
| 65 |
if (totalFrames <= 0) return;
|
| 66 |
|
|
@@ -155,6 +155,8 @@ APP.core.timeline = {};
|
|
| 155 |
// Cache duration from backend metadata (bypass video.duration dependency)
|
| 156 |
if (data.total_frames > 0 && data.fps > 0) {
|
| 157 |
APP.core.timeline._cachedDuration = data.total_frames / data.fps;
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
|
| 160 |
console.log(`[timeline] Loaded summary: ${Object.keys(frames).length} frames, duration=${APP.core.timeline._cachedDuration}s`);
|
|
|
|
| 60 |
const ctx = canvas.getContext("2d");
|
| 61 |
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
| 62 |
|
| 63 |
+
const fps = APP.core.state.hf.fps || 30;
|
| 64 |
const totalFrames = Math.ceil(duration * fps);
|
| 65 |
if (totalFrames <= 0) return;
|
| 66 |
|
|
|
|
| 155 |
// Cache duration from backend metadata (bypass video.duration dependency)
|
| 156 |
if (data.total_frames > 0 && data.fps > 0) {
|
| 157 |
APP.core.timeline._cachedDuration = data.total_frames / data.fps;
|
| 158 |
+
state.hf.fps = data.fps;
|
| 159 |
+
state.hf.totalFrames = data.total_frames;
|
| 160 |
}
|
| 161 |
|
| 162 |
console.log(`[timeline] Loaded summary: ${Object.keys(frames).length} frames, duration=${APP.core.timeline._cachedDuration}s`);
|
frontend/js/core/video.js
CHANGED
|
@@ -61,6 +61,8 @@ APP.core.video.unloadVideo = async function (options = {}) {
|
|
| 61 |
state.hf.completedJobId = null;
|
| 62 |
state.hf.asyncStatus = "idle";
|
| 63 |
state.hf.videoUrl = null;
|
|
|
|
|
|
|
| 64 |
|
| 65 |
setHfStatus("idle");
|
| 66 |
state.hasReasoned = false;
|
|
|
|
| 61 |
state.hf.completedJobId = null;
|
| 62 |
state.hf.asyncStatus = "idle";
|
| 63 |
state.hf.videoUrl = null;
|
| 64 |
+
state.hf.fps = null;
|
| 65 |
+
state.hf.totalFrames = null;
|
| 66 |
|
| 67 |
setHfStatus("idle");
|
| 68 |
state.hasReasoned = false;
|
frontend/js/main.js
CHANGED
|
@@ -166,6 +166,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
| 166 |
state.tracker.nextId = 1;
|
| 167 |
state.tracker.heatmap = {};
|
| 168 |
state.tracker.assessmentCache = {};
|
|
|
|
|
|
|
| 169 |
APP.core.timeline._cachedDuration = null;
|
| 170 |
if (btnPause) btnPause.textContent = "Pause";
|
| 171 |
renderFrameTrackList();
|
|
@@ -592,7 +594,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
| 592 |
// Backend sync every 333ms — always runs, even with zero tracks
|
| 593 |
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 594 |
if (jobId && (t - state.tracker.lastHFSync > 333)) {
|
| 595 |
-
const
|
|
|
|
|
|
|
| 596 |
if (isFinite(frameIdx) && frameIdx >= 0) {
|
| 597 |
APP.core.tracker.syncWithBackend(frameIdx);
|
| 598 |
}
|
|
|
|
| 166 |
state.tracker.nextId = 1;
|
| 167 |
state.tracker.heatmap = {};
|
| 168 |
state.tracker.assessmentCache = {};
|
| 169 |
+
state.hf.fps = null;
|
| 170 |
+
state.hf.totalFrames = null;
|
| 171 |
APP.core.timeline._cachedDuration = null;
|
| 172 |
if (btnPause) btnPause.textContent = "Pause";
|
| 173 |
renderFrameTrackList();
|
|
|
|
| 594 |
// Backend sync every 333ms — always runs, even with zero tracks
|
| 595 |
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 596 |
if (jobId && (t - state.tracker.lastHFSync > 333)) {
|
| 597 |
+
const fps = state.hf.fps || 30;
|
| 598 |
+
const maxFrame = state.hf.totalFrames ? state.hf.totalFrames - 1 : Infinity;
|
| 599 |
+
const frameIdx = Math.min(Math.floor(videoEngage.currentTime * fps), maxFrame);
|
| 600 |
if (isFinite(frameIdx) && frameIdx >= 0) {
|
| 601 |
APP.core.tracker.syncWithBackend(frameIdx);
|
| 602 |
}
|