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
44 changes: 44 additions & 0 deletions ui/src/features/agent/agentActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,23 @@ export function isExplicitVideoGenerationRequest(request: string): boolean {
return EXPLICIT_VIDEO_REQUESTS.some(pattern => pattern.test(text))
}

export function isResumePreparedStudioVideoRequest(request: string): boolean {
const text = request.trim()
if (!text || NEGATED_VIDEO_REQUEST.test(text)) return false
const match = text.match(
/\b(?:genera|generad|lanza|lanzad|encola|encolad|env[ií]a|enviad|start|queue|launch)\b[^.!?\n]{0,24}\b(?:el|la|este|esta|the)\s+(?:video|v[ií]deo|clip)\b(.*)$/i,
)
if (!match) return false
const leftover = match[1]
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[¿?¡!.,:;]/g, ' ')
.replace(/\b(?:por favor|please|ya|ahora|now)\b/g, ' ')
.replace(/\s+/g, '')
return leftover.length === 0
}

const EXPLICIT_IMAGE_REQUESTS = [
/\b(?:hazme|hacedme|generame|genérame|creame|créame)\b[^.!?\n]*\b(?:imagen|im[aá]genes|foto|fotos|retrato|ilustraci[oó]n)\b/i,
/\b(?:haz|haced|genera|generad|crea|cread|lanza|lanzad|encola|encolad)\b[^.!?\n]*\b(?:imagen|im[aá]genes|foto|fotos|retrato|ilustraci[oó]n)\b/i,
Expand Down Expand Up @@ -1843,6 +1860,33 @@ export async function reconcileAgentTurnWithRequest(
history: ExampleConversation[] = [],
): Promise<AgentTurn> {
const { maybeExampleTurn } = await import('./agentExamples')
if (isResumePreparedStudioVideoRequest(request)) {
const existing = turn.actions.find(
(action): action is AgentPrepareVideoAction => action.type === 'prepare_video',
)
const state = useStore.getState()
const studioPrompt = state.generationMode === 'video'
? String(state.params.prompt || '').trim()
: String(state.savedPromptPerMode?.video || '').trim()
const prompt = existing?.prompt.trim() || studioPrompt
if (!prompt) {
return {
reply: 'Studio no tiene un prompt preparado. Dime la escena y la encolo.',
actions: [{ type: 'open_tab', tab: 'studio' }],
}
}
const prepare: AgentPrepareVideoAction = existing || {
type: 'prepare_video',
prompt,
durationSeconds: state.generationMode === 'video' ? state.durationSeconds : undefined,
resolutionPreset: state.generationMode === 'video' ? state.resolutionPreset : undefined,
aspectRatio: state.generationMode === 'video' ? state.aspectRatio : undefined,
}
Comment thread
cursor[bot] marked this conversation as resolved.
return {
reply: 'Lanzo a la cola el vídeo ya preparado en Studio. 🪄',
actions: [prepare, { type: 'start_generation', confirm: true }],
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
const exampleTurn = maybeExampleTurn(request, turn, history)
if (exampleTurn) return exampleTurn
const rhythmic3dWorkflow = turn.actions.find(
Expand Down
85 changes: 85 additions & 0 deletions ui/tests/agentActions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1447,3 +1447,88 @@ test('a video example fills a real prompt instead of asking, and a topical video
assert.deepEqual(topical.actions.map(action => action.type), ['prepare_video', 'start_generation'])
assert.ok(topical.actions[0].prompt.includes('mapache'))
})

test('genera el video reuses a prepared Studio prompt instead of asking for a topic', async () => {
const { useStore } = await import('../src/stores/useStore.ts')
const { reconcileAgentTurnWithRequest } = await import('../src/features/agent/agentActions.ts')
const original = {
generationMode: useStore.getState().generationMode,
params: useStore.getState().params,
durationSeconds: useStore.getState().durationSeconds,
savedPromptPerMode: useStore.getState().savedPromptPerMode,
}
useStore.setState({
generationMode: 'video',
params: { ...useStore.getState().params, prompt: 'Sheldon Cooper cuenta un chiste en su salón, bata verde, Bazinga.' },
durationSeconds: 5.2,
})
const resumed = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] })
assert.deepEqual(resumed.actions.map(action => action.type), ['prepare_video', 'start_generation'])
assert.equal(resumed.actions[0].prompt.includes('Sheldon'), true)
assert.equal(resumed.actions[0].modelType, undefined)
assert.equal(resumed.actions[1].confirm, true)

useStore.setState({
generationMode: 'video',
params: { ...useStore.getState().params, prompt: '' },
savedPromptPerMode: { ...useStore.getState().savedPromptPerMode, video: '' },
})
const asked = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] })
assert.equal(asked.actions[0].type, 'open_tab')
assert.equal(asked.actions.some(action => action.type === 'start_generation'), false)
useStore.setState(original)
})

test('genera el video de un mapache uses the new topic instead of the prepared form', async () => {
const { useStore } = await import('../src/stores/useStore.ts')
const { reconcileAgentTurnWithRequest } = await import('../src/features/agent/agentActions.ts')
const original = {
generationMode: useStore.getState().generationMode,
params: useStore.getState().params,
savedPromptPerMode: useStore.getState().savedPromptPerMode,
}
useStore.setState({
generationMode: 'video',
params: { ...useStore.getState().params, prompt: 'Sheldon Cooper cuenta un chiste en su salón, bata verde, Bazinga.' },
})
const topical = await reconcileAgentTurnWithRequest('genera el video de un mapache', { reply: '¿De qué?', actions: [] })
assert.deepEqual(topical.actions.map(action => action.type), ['prepare_video', 'start_generation'])
assert.ok(topical.actions[0].prompt.includes('mapache'))
assert.equal(topical.actions[0].prompt.includes('Sheldon'), false)

const example = await reconcileAgentTurnWithRequest('genera el video de ejemplo', { reply: '¿De qué?', actions: [] })
assert.deepEqual(example.actions.map(action => action.type), ['prepare_video', 'start_generation'])
assert.ok(example.actions[0].prompt.length > 40)
assert.equal(example.actions[0].prompt.includes('genera el video'), false)
assert.equal(example.actions[0].prompt.includes('Sheldon'), false)
useStore.setState(original)
})

test('genera el video does not copy an incompatible I2V, audio or 3D model as T2V', async () => {
const { useStore } = await import('../src/stores/useStore.ts')
const { reconcileAgentTurnWithRequest } = await import('../src/features/agent/agentActions.ts')
const original = {
generationMode: useStore.getState().generationMode,
params: useStore.getState().params,
savedPromptPerMode: useStore.getState().savedPromptPerMode,
}
const prepared = 'Sheldon Cooper cuenta un chiste en su salón, bata verde, Bazinga.'

for (const [generationMode, modelType] of [
['image', 'image-only-model'],
['audio', 'ace_step_music'],
['model3d', 'hunyuan3d'],
['video', 'i2v-only-model'],
]) {
useStore.setState({
generationMode,
params: { ...useStore.getState().params, model_type: modelType, prompt: generationMode === 'video' ? prepared : 'otro modo' },
savedPromptPerMode: { ...useStore.getState().savedPromptPerMode, video: prepared },
})
const turn = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] })
assert.deepEqual(turn.actions.map(action => action.type), ['prepare_video', 'start_generation'])
assert.equal(turn.actions[0].modelType, undefined, `${generationMode}/${modelType} must not be copied as T2V`)
assert.equal(turn.actions[0].prompt.includes('Sheldon'), true)
}
useStore.setState(original)
})