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",
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,