From 788df7424d1484708f44366ec80d0074f29267e5 Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Tue, 29 Sep 2026 23:03:54 +1000 Subject: [PATCH] fix: keep the timeline length when a generated clip replaces its placeholder --- src/core/player-reconciler.ts | 2 ++ tests/generation-public-api.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/core/player-reconciler.ts b/src/core/player-reconciler.ts index 99be26ed..97a23036 100644 --- a/src/core/player-reconciler.ts +++ b/src/core/player-reconciler.ts @@ -153,6 +153,8 @@ export class PlayerReconciler { // Sync track containers AFTER players are disposed and tracks rebuilt // This ensures empty tracks are correctly identified for removal this.syncTrackContainers(resolved.timeline.tracks.length); + // Disposal recalculates the duration before replacement players exist; recalculate once they do. + this.edit.updateTotalDuration(); return result; } finally { diff --git a/tests/generation-public-api.test.ts b/tests/generation-public-api.test.ts index 62deefe9..885cbe4a 100644 --- a/tests/generation-public-api.test.ts +++ b/tests/generation-public-api.test.ts @@ -164,6 +164,30 @@ describe("generation through the public API", () => { edit.dispose(); }); + it("keeps the timeline length when the last clip's generated asset replaces its placeholder", async () => { + const edit = new Edit({ + timeline: { + tracks: [ + { + clips: [ + { asset: { type: "audio", prompt: "a calm harbour at dusk" }, start: 0, length: 5 }, + { asset: { type: "audio", prompt: "gulls over the water" }, start: 5, length: 5 } + ] + } + ] + }, + output: { size: { width: 1920, height: 1080 }, format: "mp4" } + }); + await edit.load(); + const last = (edit.getEdit({ includeIds: true }).timeline.tracks[0]?.clips[1] as Clip & { id: string }).id; + edit.registerAssetGenerator(async () => ({ url: "https://cdn.example.com/gulls.mp3" })); + + await edit.generateClip(last); + + expect(edit.totalDuration).toBe(10); + edit.dispose(); + }); + it("reports the handler's message on failure, and resolves rather than rejecting", async () => { const edit = await editWithPromptClip(); const failures: Array<{ clipId: string; error: string }> = [];