From 7e66b400bfff3a5a1203348b610700941d66abbe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 22:14:33 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(studio):=20no=20lanzar=20la=20GPU=20des?= =?UTF-8?q?de=20la=20pesta=C3=B1a=20Mixer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El enrutado de comandos compartidos solo admite speech/music/sfx. Mixer caía al submitGeneration legado con el model_type y el prompt del tab anterior, así que Generate arrancaba una generación de Voz/Música/SFX. Co-authored-by: ignaciodelcano+dcl --- ui/src/components/Sidebar/Sidebar.tsx | 8 ++- ui/src/features/studio/studioSubmission.ts | 6 ++ ui/src/i18n/locales/en/studio.json | 3 +- ui/src/i18n/locales/es/studio.json | 3 +- ui/src/stores/useStore.ts | 3 + ui/tests/studioSubmissionFailure.test.ts | 78 +++++++++++++++++----- 6 files changed, 79 insertions(+), 22 deletions(-) diff --git a/ui/src/components/Sidebar/Sidebar.tsx b/ui/src/components/Sidebar/Sidebar.tsx index 2648e6fe..645dbab9 100644 --- a/ui/src/components/Sidebar/Sidebar.tsx +++ b/ui/src/components/Sidebar/Sidebar.tsx @@ -305,9 +305,11 @@ export function Sidebar() {
-
- -
+ {!(isAudio && audioSubMode === 'mixer') && ( +
+ +
+ )} )} diff --git a/ui/src/features/studio/studioSubmission.ts b/ui/src/features/studio/studioSubmission.ts index 96486c2a..53d4e30e 100644 --- a/ui/src/features/studio/studioSubmission.ts +++ b/ui/src/features/studio/studioSubmission.ts @@ -1,4 +1,5 @@ import * as api from '../../api/client' +import i18n from '../../i18n' type Preparation = typeof import('./imageCommandSubmission').prepareStudioSubmission type Inputs = Parameters @@ -33,6 +34,11 @@ export async function prepareStudioSubmission( const implementation = await import('./sfxCommandSubmission') return await implementation.prepareStudioSfxSubmission(params, before, current, context, referenceErrors) } + if (before.generationMode === 'audio') { + // Mixer is ffmpeg-only. An unknown Audio tab must not inherit leftover + // Speech/Music/SFX params and POST them to the legacy GPU endpoint. + throw new Error(i18n.t('studio:commands.audioNotGenerative')) + } } catch (error) { return { params, submit: () => Promise.reject(error) } } diff --git a/ui/src/i18n/locales/en/studio.json b/ui/src/i18n/locales/en/studio.json index 176e408b..ac2ec637 100644 --- a/ui/src/i18n/locales/en/studio.json +++ b/ui/src/i18n/locales/en/studio.json @@ -939,7 +939,8 @@ "panelUnavailable": "The image request could not be shown. Open Studio and try again.", "pendingInvalid": "The saved submission could not be read. It has been preserved for recovery.", "referenceFailed": "A selected reference could not be prepared. No image was submitted.", - "conflictingGuides": "Conflicting image guides are selected. Choose one control image and mask before generating." + "conflictingGuides": "Conflicting image guides are selected. Choose one control image and mask before generating.", + "audioNotGenerative": "This Audio tab does not start a generation. Use Speech, Music or SFX, or Mix & Save in Mixer." }, "speechCommands": { "prepared": "Speech request ready", diff --git a/ui/src/i18n/locales/es/studio.json b/ui/src/i18n/locales/es/studio.json index 9df6f163..d350b524 100644 --- a/ui/src/i18n/locales/es/studio.json +++ b/ui/src/i18n/locales/es/studio.json @@ -939,7 +939,8 @@ "panelUnavailable": "No se pudo mostrar la solicitud de imagen. Abre Studio y vuelve a intentarlo.", "pendingInvalid": "No se pudo leer la solicitud guardada. Se ha conservado para recuperarla.", "referenceFailed": "No se pudo preparar una referencia seleccionada. No se ha enviado ninguna imagen.", - "conflictingGuides": "Hay guías de imagen distintas seleccionadas. Elige una imagen de control y una máscara antes de generar." + "conflictingGuides": "Hay guías de imagen distintas seleccionadas. Elige una imagen de control y una máscara antes de generar.", + "audioNotGenerative": "Esta pestaña de Audio no inicia una generación. Usa Voz, Música o SFX, o Mezclar y guardar en el Mezclador." }, "speechCommands": { "prepared": "Solicitud de voz preparada", diff --git a/ui/src/stores/useStore.ts b/ui/src/stores/useStore.ts index b5178284..0eb80a95 100644 --- a/ui/src/stores/useStore.ts +++ b/ui/src/stores/useStore.ts @@ -4121,6 +4121,9 @@ export const useStore = create((set, get) => { isGenerating: false, startGeneration: async (scheduledPrompt, submissionContext) => { const initialState = get() + if (initialState.generationMode === 'audio' && initialState.audioSubMode === 'mixer') { + throw new Error(i18n.t('studio:commands.audioNotGenerative')) + } // Studio Prompt Scheduler: each non-empty line becomes its own normal // generation request. Submitting the requests one at a time preserves the diff --git a/ui/tests/studioSubmissionFailure.test.ts b/ui/tests/studioSubmissionFailure.test.ts index 98dd84af..8d6a1810 100644 --- a/ui/tests/studioSubmissionFailure.test.ts +++ b/ui/tests/studioSubmissionFailure.test.ts @@ -22,27 +22,71 @@ test('a missing image chunk produces a failed submission for the existing job er assert.equal(requests, 0) }) -for (const mode of ['audio', 'video']) { - test(`${mode} submits through its native API without loading image code`, async () => { - const params = { prompt: 'literal\nsecond line', generation_mode: mode, workspace: 'command-qa' } - const before = state(mode, params) - let loads = 0 - let sent: unknown - globalThis.fetch = async (_url, options) => { - sent = JSON.parse(String(options?.body)) - return new Response(JSON.stringify({ job_id: 'native-job', status: 'queued' }), { status: 200 }) +test('video submits through its native API without loading image code', async () => { + const params = { prompt: 'literal\nsecond line', generation_mode: 'video', workspace: 'command-qa' } + const before = state('video', params) + let loads = 0 + let sent: unknown + globalThis.fetch = async (_url, options) => { + sent = JSON.parse(String(options?.body)) + return new Response(JSON.stringify({ job_id: 'native-job', status: 'queued' }), { status: 200 }) + } + const submission = await prepareStudioSubmission(params, before, () => before, undefined, [], async () => { + loads += 1 + throw new Error('Image chunk unavailable') + }) + const result = await submission.submit() + assert.equal(result.job_id, 'native-job') + assert.equal(loads, 0) + assert.deepEqual(sent, params) +}) + +function audioState(subMode: string, params: Record) { + return { ...state('audio', params), audioSubMode: subMode } as Parameters[1] +} + +for (const subMode of ['mixer', '', undefined]) { + test(`audio ${subMode === undefined ? 'without a sub-mode' : `tab ${JSON.stringify(subMode)}`} does not fall back to the legacy GPU endpoint`, async () => { + const params = { + prompt: 'leftover speech lyrics', + model_type: 'kugelaudio_0_open', + generation_mode: 'audio', + workspace: 'command-qa', + _audio_sub_mode: subMode ?? 'mixer', } - const submission = await prepareStudioSubmission(params, before, () => before, undefined, [], async () => { - loads += 1 - throw new Error('Image chunk unavailable') - }) - const result = await submission.submit() - assert.equal(result.job_id, 'native-job') - assert.equal(loads, 0) - assert.deepEqual(sent, params) + const before = subMode === undefined ? state('audio', params) : audioState(subMode, params) + let requests = 0 + globalThis.fetch = async () => { requests += 1; throw new Error('Unexpected POST') } + const submission = await prepareStudioSubmission(params, before, () => before) + await assert.rejects(submission.submit, /does not start a generation|no inicia una generación/) + assert.equal(requests, 0) }) } +test('startGeneration refuses Mixer before creating a job or POSTing', { concurrency: false }, async () => { + const { useStore } = await import('../src/stores/useStore.ts') + const before = useStore.getState() + let requests = 0 + globalThis.fetch = async () => { requests += 1; throw new Error('Unexpected POST') } + useStore.setState({ + generationMode: 'audio', + audioSubMode: 'mixer', + params: { ...before.params, model_type: 'kugelaudio_0_open', prompt: 'leftover speech lyrics' }, + jobs: [], + llmStatus: { ...before.llmStatus, loaded: false }, + }) + try { + await assert.rejects( + () => useStore.getState().startGeneration(), + /does not start a generation|no inicia una generación/, + ) + assert.equal(useStore.getState().jobs.length, 0) + assert.equal(requests, 0) + } finally { + useStore.setState(before) + } +}) + test('legacy image control and mask fields are translated in the detached V2 snapshot', async () => { const params = { workspace: 'command-qa', prompt: 'Use this control image literally', model_type: 'pi_flux2', resolution: '512x512', num_inference_steps: 4, seed: 42, guidance_scale: 1, From 0abf34f42810d99a907698340f6cd2d24f4bf750 Mon Sep 17 00:00:00 2001 From: IAnMove <216241348+IAnMove@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:58:27 +0200 Subject: [PATCH 2/2] test(studio): register Mixer submission behavior dependency --- tests/fixtures/architecture_wire_inventory.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/fixtures/architecture_wire_inventory.json b/tests/fixtures/architecture_wire_inventory.json index 7016545f..fb5ecd92 100644 --- a/tests/fixtures/architecture_wire_inventory.json +++ b/tests/fixtures/architecture_wire_inventory.json @@ -631,6 +631,12 @@ "classification": "behavior", "reason": "Imports the public Zustand facade; it should survive slice extraction unchanged." }, + { + "file": "ui/tests/studioSubmissionFailure.test.ts", + "target": "ui/src/stores/useStore.ts", + "classification": "behavior", + "reason": "Imports the public Zustand facade; it should survive slice extraction unchanged." + }, { "file": "ui/tests/tabFilterSearch.test.tsx", "target": "ui/src/stores/useStore.ts",