Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ui/src/features/studio/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
PrepareVideoCommand,
QueueSfxPackCommand,
} from './commands'
import { newSfxGenerationIntentId } from '../../api/sfxGenerationCommands'
import {
generationProvenancePayload,
type GenerationSubmissionContext,
Expand Down Expand Up @@ -127,6 +128,13 @@ export async function selectAudioModel(
return selectedModel?.name || selected
}

function sfxClipSubmissionContext(
context?: GenerationSubmissionContext,
): GenerationSubmissionContext | undefined {
if (!context) return undefined
return { ...context, commandId: newSfxGenerationIntentId() }
}

export async function queueSfxPack(
action: QueueSfxPackCommand,
context?: GenerationSubmissionContext,
Expand All @@ -140,7 +148,10 @@ export async function queueSfxPack(
for (const clip of action.clips) {
applySfxClip(clip, negative)
const before = new Set(useStore.getState().jobs)
await useStore.getState().startGeneration(undefined, context)
// Wizard issues one commandId for the pack capability. Reusing it as
// every clip's generation.sfx intent_id 409s distinct prompts and
// silently replays the first receipt when prompts match.
await useStore.getState().startGeneration(undefined, sfxClipSubmissionContext(context))
const created = useStore.getState().jobs.find(job => !before.has(job))
if (!created) throw new Error(`HocusPocus no encoló el efecto ${clip.name}.`)
if (created.status === 'failed') throw new Error(created.error || created.message || `Falló ${clip.name}.`)
Expand Down
30 changes: 29 additions & 1 deletion ui/tests/studioSfxAction.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ window.matchMedia = () => ({ matches: false })

const { parseAgentTurn } = await import('../src/features/agent/agentActions.ts')
const { getCapability } = await import('../src/features/agent/capabilityRegistry.ts')
const { prepareAudio } = await import('../src/features/studio/actions.ts')
const { prepareAudio, queueSfxPack } = await import('../src/features/studio/actions.ts')
const { useStore } = await import('../src/stores/useStore.ts')
const guide = '/api/v1/file/guide.mp4?workspace=source'
const literal = ' Rain on a tin roof.\nKeep this exact. '
Expand Down Expand Up @@ -145,3 +145,31 @@ for (const settleWithFailure of [false, true]) {
}
})
}

test('queue_sfx_pack mints a unique generation.sfx intent for each clip', async () => {
await withStudio(async () => {
const seen = []
useStore.setState({
jobs: [],
loadModelOptions: async () => undefined,
startGeneration: async (_scheduled, context) => {
seen.push(context?.commandId)
const job = { id: `job-${seen.length}`, status: 'queued' }
useStore.setState(state => ({ jobs: [job, ...state.jobs] }))
},
})
await queueSfxPack({
type: 'queue_sfx_pack',
confirm: true,
clips: [
{ name: 'hit', prompt: 'impact', durationSeconds: 2 },
{ name: 'whoosh', prompt: 'whoosh', durationSeconds: 3 },
{ name: 'hit-again', prompt: 'impact', durationSeconds: 2 },
],
modelType: 'mmaudio_v2',
}, { actor: 'wizard', capability: 'queue_sfx_pack', commandId: 'pack-intent' })
assert.equal(seen.length, 3)
assert.equal(new Set(seen).size, 3)
assert.ok(seen.every(id => typeof id === 'string' && id.length > 0 && id !== 'pack-intent'))
})
})