Movie Editor: generated renders are immutable, full-length (no truncation by plan changes)
A render never recorded the length it was actually made at — sceneDurationSec recomputed from the LIVE plan every time, so changing project/scene length retroactively truncated already-generated videos and made their tail unreachable in the preview, player, and final export alike. Now each render freezes its real length at generation time (durationSec). A generated scene in project (default) frames mode plays/lays-out/exports at that frozen length; the project/scene default only drives the NEXT generation. Explicit per-scene length (timeline/custom), slip-trim, and splits still win (a split clears the frozen length so both halves revert to plan layout). - store: renderDurationSec + _planDurationSec; sceneDurationSec is render-aware; _recordSegment freezes durationSec on scene renders and ghosts; trim/resize measure from the shown length; segmentDurationSec delegates ghost length to _ghostDurationSec (de-dupes the inline calc). - inspector: scene duration hint shows 'Rendered ~Xs - plan Ys - Regenerate to apply' when a render is frozen at an older plan length. Centralized in sceneDurationSec so timeline, player, audio offsets/durations and the final stitch all agree. No-op in the common case (plan == render). Renders made BEFORE this change have no frozen length and stay plan-driven until regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6d25c8c1c5
commit
0c7fe14b79
@@ -275,7 +275,14 @@
|
||||
}
|
||||
|
||||
const effFrames = effOf(scene, "frames"), effFps = effOf(scene, "fps") || 1;
|
||||
body.append(el("div", "insp-hint", `Duration ≈ ${(effFrames / effFps).toFixed(2)}s · trim on timeline · Convert to video on the timeline toolbar · generation splits: global prompt or Outgoing seam below`));
|
||||
const planDur = effFrames / effFps;
|
||||
const shownDur = S.sceneDurationSec ? S.sceneDurationSec(scene) : planDur;
|
||||
const lenDrift = Math.abs(shownDur - planDur) > 0.05; // render frozen at an older plan length
|
||||
body.append(el("div", "insp-hint",
|
||||
(lenDrift
|
||||
? `Rendered ≈ ${shownDur.toFixed(2)}s · plan ${planDur.toFixed(2)}s — Regenerate to apply`
|
||||
: `Duration ≈ ${planDur.toFixed(2)}s`)
|
||||
+ ` · trim on timeline · Convert to video on the timeline toolbar · generation splits: global prompt or Outgoing seam below`));
|
||||
|
||||
renderOutgoingSeam(st, scene);
|
||||
|
||||
|
||||
@@ -286,6 +286,7 @@
|
||||
state.sceneRenders[id] = {
|
||||
media: ghost.media,
|
||||
inSec: ghost.inSec || 0,
|
||||
durationSec: ghost.durationSec != null ? ghost.durationSec : undefined,
|
||||
};
|
||||
}
|
||||
return sc;
|
||||
@@ -1359,10 +1360,7 @@
|
||||
if (isVideoClip(sc) && sc.source_dur != null) return sc.source_dur;
|
||||
return sceneDurationSec(sc);
|
||||
}
|
||||
const g = seg.ghost;
|
||||
const fps = (g.fps_mode !== "project" && g.fps != null) ? g.fps : p.frame_rate;
|
||||
const frames = (g.frames_mode !== "project" && g.frames != null) ? g.frames : p.num_frames_per_scene;
|
||||
return (frames || 1) / (fps || 25);
|
||||
return _ghostDurationSec(seg.ghost);
|
||||
}
|
||||
|
||||
// Preview/timeline layout: live scenes interleaved with removed-scene ghosts.
|
||||
@@ -1493,7 +1491,7 @@
|
||||
return;
|
||||
}
|
||||
const fps = sceneEffFps(s);
|
||||
const curFrames = sceneEffFrames(s);
|
||||
const curFrames = Math.max(1, Math.round(sceneDurationSec(s) * fps)); // trim from what's shown
|
||||
const trimFrames = Math.max(1, Math.round(trim * fps));
|
||||
const rawNext = curFrames - trimFrames;
|
||||
if (rawNext < 9) return;
|
||||
@@ -1522,8 +1520,16 @@
|
||||
function redo() { window.EditorHistory?.redo(window.Store); }
|
||||
|
||||
|
||||
// A scene's duration in seconds (respecting per-scene frames/fps modes).
|
||||
function sceneDurationSec(sc) {
|
||||
// Length (s) this scene was ACTUALLY rendered at, frozen at generation time. Null until
|
||||
// generated (or for old renders saved before this was captured — they fall back to plan).
|
||||
function renderDurationSec(sceneId) {
|
||||
const r = state.sceneRenders[sceneId];
|
||||
return r && r.durationSec != null ? r.durationSec : null;
|
||||
}
|
||||
|
||||
// The PLANNED length (s) from the per-scene frames/fps modes — what the next Generate will
|
||||
// produce. Ignores any existing render. Use this for "what will generate", not playback.
|
||||
function _planDurationSec(sc) {
|
||||
const p = state.project; if (!p || !sc) return 0;
|
||||
if (isVideoClip(sc) && sc.source_dur != null) return sc.source_dur;
|
||||
const fps = sceneEffFps(sc, p) || 25;
|
||||
@@ -1531,6 +1537,21 @@
|
||||
return frames / fps;
|
||||
}
|
||||
|
||||
// A scene's duration in seconds for layout / playback / trimming. A generated render is
|
||||
// IMMUTABLE: once made it keeps the length it was rendered at, so changing the project or
|
||||
// scene default can never truncate it or make its tail unreachable — regenerate or trim to
|
||||
// change it. An explicit per-scene length (timeline/custom mode), a slip-trim duration, or
|
||||
// a split (which clears the frozen length) all take precedence and fall back to the plan.
|
||||
function sceneDurationSec(sc) {
|
||||
if (!sc) return 0;
|
||||
const rd = renderDurationSec(sc.id);
|
||||
if (rd != null && !isVideoClip(sc) && sc.source_dur == null
|
||||
&& (sc.frames_mode || "project") === "project") {
|
||||
return rd;
|
||||
}
|
||||
return _planDurationSec(sc);
|
||||
}
|
||||
|
||||
// Insert or adjust a black/silent pause after this clip (seconds).
|
||||
function setSceneGapAfter(id, sec) {
|
||||
if (!state.project) return;
|
||||
@@ -1555,7 +1576,7 @@
|
||||
s.source_dur = target;
|
||||
s.frames = snapFrames(target * fps);
|
||||
} else {
|
||||
const curFrames = sceneEffFrames(s);
|
||||
const curFrames = Math.max(1, Math.round(sceneDurationSec(s) * fps)); // resize from what's shown
|
||||
const nextFrames = _framesFromDuration(target, fps, curFrames);
|
||||
if (nextFrames === curFrames) return;
|
||||
s.frames = nextFrames;
|
||||
@@ -1608,6 +1629,7 @@
|
||||
// at the first half's out-point (its in-point + first-half duration).
|
||||
const r = state.sceneRenders[id];
|
||||
if (r && r.media) {
|
||||
delete r.durationSec; // a split is an explicit re-length: both halves revert to plan layout
|
||||
state.sceneRenders[second.id] = {
|
||||
media: r.media,
|
||||
inSec: (r.inSec || 0) + cutSec,
|
||||
@@ -2509,6 +2531,8 @@
|
||||
}
|
||||
|
||||
function _ghostDurationSec(g) {
|
||||
if (!g) return 0;
|
||||
if (g.durationSec != null) return g.durationSec; // frozen real length once rendered
|
||||
const p = state.project;
|
||||
const fps = (g.fps_mode !== "project" && g.fps != null) ? g.fps : p.frame_rate;
|
||||
const frames = (g.frames_mode !== "project" && g.frames != null) ? g.frames : p.num_frames_per_scene;
|
||||
@@ -2688,10 +2712,11 @@
|
||||
const ghost = ghosts[gi];
|
||||
const sameUnit = lastChain && genUnitId(lastChain) === genUnitId(ghost);
|
||||
const inSec = sameUnit ? sourceEnd : _recordInSec(lastChain, sourceEnd, ghost, sceneLayout, chainSceneIdx, sourceSpanSec);
|
||||
ghosts[gi] = { ...ghost, media: primary, inSec, pendingGen: false };
|
||||
const ghostDur = _ghostDurationSec(ghost); // freeze before any project change can shrink it
|
||||
ghosts[gi] = { ...ghost, media: primary, inSec, durationSec: ghostDur, pendingGen: false };
|
||||
state.sceneGhosts = ghosts;
|
||||
completedGhostIds.push(id);
|
||||
sourceEnd = inSec + _ghostDurationSec(ghost);
|
||||
sourceEnd = inSec + ghostDur;
|
||||
lastChain = ghost;
|
||||
if (!sameUnit) chainSceneIdx += 1;
|
||||
}
|
||||
@@ -2703,11 +2728,12 @@
|
||||
const inSec = sameUnit ? sourceEnd : _recordInSec(lastChain, sourceEnd, sc, sceneLayout, chainSceneIdx, sourceSpanSec);
|
||||
const renderPrompt = _queuedRenderPrompts[id] || _snapshotRenderPrompt(id);
|
||||
delete _queuedRenderPrompts[id];
|
||||
state.sceneRenders[id] = { media: primary, inSec, renderPrompt };
|
||||
const realDur = _planDurationSec(sc); // freeze the length this render was actually made at
|
||||
state.sceneRenders[id] = { media: primary, inSec, renderPrompt, durationSec: realDur };
|
||||
recordedSceneIds.push(id);
|
||||
const root = genUnitRoot(genUnitId(sc));
|
||||
if (root && root.rating) { root.rating = ""; clearedRating = true; }
|
||||
sourceEnd = inSec + sceneDurationSec(sc);
|
||||
sourceEnd = inSec + realDur;
|
||||
lastChain = sc;
|
||||
if (!sameUnit) chainSceneIdx += 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user