From 6f1b45409051942fbf1d4431de15f40a72a4598a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 06:20:35 +0000 Subject: [PATCH] fix(studio): preserve DramaBox auto duration instead of clamping it to 1s setDurationSeconds used a video floor of 1s, so Wizard prepare and the Advanced slider rewrote the TTS 0=auto sentinel into a one-second clip. Co-authored-by: ignaciodelcano+dcl --- ui/src/stores/studioConfigurationSlice.ts | 13 ++++++- ui/tests/architectureSlices.test.mjs | 42 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/ui/src/stores/studioConfigurationSlice.ts b/ui/src/stores/studioConfigurationSlice.ts index 08a95026..a7477788 100644 --- a/ui/src/stores/studioConfigurationSlice.ts +++ b/ui/src/stores/studioConfigurationSlice.ts @@ -133,7 +133,18 @@ export function createStudioConfigurationSlice( setDurationSeconds: requestedSeconds => { const options = get().modelOptions const fps = options?.fps ?? 16 - const minimum = Math.max(1, (options?.frames_minimum || fps) / fps) + // DramaBox (and similar TTS models) declare duration_slider.min=0 so + // that 0 means "auto-derive from the prompt". The video floor of 1s + // must not rewrite that sentinel — Wizard prepare and the Advanced + // slider both call this setter with 0. + const autoDurationAllowed = Boolean( + options?.audio_only + && options.duration_slider + && options.duration_slider.min === 0, + ) + const minimum = autoDurationAllowed + ? 0 + : Math.max(1, (options?.frames_minimum || fps) / fps) const nativeMaximum = options?.frames_maximum ? options.frames_maximum / fps : null const maximum = options?.sliding_window || nativeMaximum == null ? Number.POSITIVE_INFINITY diff --git a/ui/tests/architectureSlices.test.mjs b/ui/tests/architectureSlices.test.mjs index cc04c10f..ba5cb2cf 100644 --- a/ui/tests/architectureSlices.test.mjs +++ b/ui/tests/architectureSlices.test.mjs @@ -393,6 +393,48 @@ test('Studio configuration slice owns form state without owning generation execu assert.equal('runTool' in studio, false) }) +test('Studio duration setter keeps TTS auto sentinel at zero', async () => { + const { createStudioConfigurationSlice } = await import('../src/stores/studioConfigurationSlice.ts') + let state = { + params: { prompt: 'Speaker 1: hello', image_mode: 0 }, + modelOptions: { + fps: 16, + audio_only: true, + duration_slider: { label: 'Target Duration (seconds, 0 = auto)', min: 0, max: 120, increment: 0.5, default: 0 }, + }, + h3WindowPlan: null, + generationMode: 'audio', + savedParamsPerMode: {}, + } + const set = update => { + const partial = typeof update === 'function' ? update(state) : update + state = { ...state, ...partial } + } + const studio = createStudioConfigurationSlice({ + alignFrameCount: frames => frames, + resolveResolution: (_options, preset, ratio) => `${preset}:${ratio}`, + })(set, () => state) + state = { ...state, ...studio } + + state.setDurationSeconds(0) + assert.equal(state.durationSeconds, 0) + assert.equal(state.params.video_length, 0) + + state.setDurationSeconds(12) + assert.equal(state.durationSeconds, 12) + + state.modelOptions = { + fps: 24, + frames_minimum: 24, + frames_maximum: 120, + sliding_window: true, + audio_only: false, + duration_slider: null, + } + state.setDurationSeconds(0) + assert.equal(state.durationSeconds, 1) +}) + test('composed slices bind without as-never casts at the useStore call site', async () => { const fs = await import('node:fs/promises') const source = await fs.readFile(new URL('../src/stores/useStore.ts', import.meta.url), 'utf8')