diff --git a/movie_editor/frontend/player.js b/movie_editor/frontend/player.js index 467938df..ada4c27e 100644 --- a/movie_editor/frontend/player.js +++ b/movie_editor/frontend/player.js @@ -117,7 +117,7 @@ } function _syncPool() { - const urls = new Set(_clips.map((c) => _urlFor(c.media))); + const urls = new Set(_clips.filter((c) => c.media).map((c) => _urlFor(c.media))); urls.forEach((u) => _ensureVideo(u)); for (const [u, v] of [...pool]) { if (urls.has(u)) continue; @@ -207,11 +207,18 @@ const end = _currentClip.startSec + _currentClip.durationSec; const next = _clips.find((c) => !c.pending && c.startSec >= end - 0.02 && c !== _currentClip); if (!next) { _phSec = end; _pause(); return; } - const contiguous = _urlFor(next.media) === _urlFor(_currentClip.media) - && Math.abs((next.inSec || 0) - ((_currentClip.inSec || 0) + _currentClip.durationSec)) < 0.05; + const prev = _currentClip; + const sameSource = next.media && prev.media + && _urlFor(next.media) === _urlFor(prev.media); + const contiguous = sameSource + && Math.abs((next.inSec || 0) - ((prev.inSec || 0) + prev.durationSec)) < 0.05; _phSec = next.startSec; _currentClip = next; - if (!contiguous) _goto(next, 0, _playing); // jump (different source / non-contiguous) + // Reseek on gen-unit / scene boundaries — chain carry scenes overlap in the source file. + const needSeek = !sameSource || !contiguous + || (next.sceneId && prev.sceneId && next.sceneId !== prev.sceneId) + || (next.ghostId && prev.ghostId && next.ghostId !== prev.ghostId); + if (needSeek) _goto(next, 0, _playing); _notifyPh(); } @@ -229,7 +236,10 @@ if (_playing) _raf = requestAnimationFrame(_tick); return; } - if (!_active) return; + if (!_active) { + if (_playing) _pause(); + return; + } const within = _active.currentTime - (_currentClip.inSec || 0); if (within >= _currentClip.durationSec - 0.001) { _advance(); } else { _phSec = _currentClip.startSec + Math.max(0, within); try { _applyFx(_currentClip, within); } catch (_) {} _notifyPh(); } diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index 3c958197..d6955fe5 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -552,20 +552,33 @@ state.sceneGhosts = (state.sceneGhosts || []).filter((g) => !ids.has(g.afterSceneId) && !ids.has(g.id)); } + // Pixel overlap between consecutive chain-sampler scenes (carry/i2v guide runs). + function _chainOverlapSec(runSceneCount) { + if (!runSceneCount || runSceneCount < 2) return 0; + const frames = +(state.project?.sampler_inputs?.frame_overlap ?? 16); + const fps = state.project?.frame_rate || 25; + return Math.max(0, frames / fps); + } + function _recordSegment(mediaList, targetSceneIds) { if (!mediaList || !mediaList.length || !targetSceneIds || !targetSceneIds.length) return; const primary = mediaList.find((m) => m.kind === "videos" || m.kind === "gifs") || mediaList[0]; - let inAcc = 0; + const overlapSec = _chainOverlapSec(targetSceneIds.length); + let sourceEnd = 0; let clearedRating = false; _pruneGhostsAfterRegen(targetSceneIds); - for (const id of targetSceneIds) { + for (let i = 0; i < targetSceneIds.length; i++) { + const id = targetSceneIds[i]; const sc = scene(id); if (!sc) continue; - state.sceneRenders[id] = { media: primary, inSec: inAcc }; + const prev = i > 0 ? scene(targetSceneIds[i - 1]) : null; + const sameUnit = prev && genUnitId(prev) === genUnitId(sc); + const inSec = i === 0 ? 0 : (sameUnit ? sourceEnd : Math.max(0, sourceEnd - overlapSec)); + state.sceneRenders[id] = { media: primary, inSec }; // New render invalidates the prior RLHF rating — UI shows "— rate —"; server // uses continue-refinement (session memory, no new rating) on the next Studio run. const root = genUnitRoot(genUnitId(sc)); if (root && root.rating) { root.rating = ""; clearedRating = true; } - inAcc += sceneDurationSec(sc); + sourceEnd = inSec + sceneDurationSec(sc); } if (clearedRating) scheduleSaveSilent(); }