Skip to content
Closed
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
57 changes: 30 additions & 27 deletions ui/src/features/agent/agentActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1870,13 +1870,13 @@ export function isExplicitRetryRequest(request: string): boolean {

export function isExplicitVideoGenerationRequest(request: string): boolean {
const text = request.trim()
if (!text || NEGATED_VIDEO_REQUEST.test(text)) return false
if (!text || NEGATED_VIDEO_REQUEST.test(text) || isHowToGenerateQuestion(text)) return false
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
if (!text || NEGATED_VIDEO_REQUEST.test(text) || isHowToGenerateQuestion(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,
)
Expand All @@ -1900,7 +1900,7 @@ const EXPLICIT_IMAGE_REQUESTS = [

export function isExplicitImageGenerationRequest(request: string): boolean {
const text = request.trim()
if (!text || NEGATED_VIDEO_REQUEST.test(text)) return false
if (!text || NEGATED_VIDEO_REQUEST.test(text) || isHowToGenerateQuestion(text)) return false
if (isExplicitVideoGenerationRequest(text)) return false
return EXPLICIT_IMAGE_REQUESTS.some(pattern => pattern.test(text))
}
Expand All @@ -1920,7 +1920,7 @@ const STUDIO_AUDIO_CONTEXT = [
export function isExplicitAudioGenerationRequest(request: string): boolean {
const text = request.trim()
if (!text || NEGATED_VIDEO_REQUEST.test(text) || MUSIC_VIDEO_CONTEXT.test(text)) return false
if (isExplicitSfxGenerationRequest(text)) return false
if (isHowToGenerateQuestion(text) || isExplicitSfxGenerationRequest(text)) return false
if (!STUDIO_AUDIO_CONTEXT.some(pattern => pattern.test(text))) return false
return EXPLICIT_AUDIO_GENERATION_REQUESTS.some(pattern => pattern.test(text))
}
Expand All @@ -1941,7 +1941,7 @@ const EXPLICIT_3D_REQUESTS = [

export function isExplicit3dGenerationRequest(request: string): boolean {
const text = request.trim()
if (!text || NEGATED_VIDEO_REQUEST.test(text)) return false
if (!text || NEGATED_VIDEO_REQUEST.test(text) || isHowToGenerateQuestion(text)) return false
if (isExplicitVideoGenerationRequest(text) || isExplicitImageGenerationRequest(text) || isExplicitSfxGenerationRequest(text)) return false
return EXPLICIT_3D_REQUESTS.some(pattern => pattern.test(text))
}
Expand Down Expand Up @@ -2015,8 +2015,11 @@ const HOW_TO_GENERATE = [

export function isHowToGenerateQuestion(request: string): boolean {
const text = request.trim()
if (!text || text.length > 240) return false
return HOW_TO_GENERATE.some(pattern => pattern.test(text))
if (!text) return false
// Classify from the opening window so a long explanation after
// "how do I generate a video?" stays educational. A later command
// after 240 characters is treated as a separate request.
return HOW_TO_GENERATE.some(pattern => pattern.test(text.slice(0, 240)))
}

const LABS_INVENTORY = /(?:¿\s*)?(?:qu[eé]\s+puedes\s+hacer|what\s+can\s+you\s+do)(?:\s+(?:en|in|con|with))?\s+(?:el\s+)?(?:series\s+lab|story\s+lab)/i
Expand Down Expand Up @@ -2145,6 +2148,26 @@ export async function reconcileAgentTurnWithRequest(
}
: candidate
turn = preserveExactEpisodeTitle(turn)
if (isComicLaunchHowQuestion(request, history)) {
return {
reply: [
'No hay un botón llamado **Render page**.',
'El dibujo de las viñetas es **Generate all images** en Comic Director (barra de Comics), o dímelo aquí: **lánzalo**.',
'Las viñetas entran en la **misma GPU**, una detrás de otra, no en paralelo. No es un segundo motor.',
].join('\n\n'),
actions: [{ type: 'open_tab', tab: 'comics' }],
}
}
if (isHowToGenerateQuestion(request)) {
return {
...turn,
actions: turn.actions.filter(action => (
action.type === 'open_tab'
|| action.type === 'open_story_section'
|| action.type === 'open_series_section'
)),
}
}
if (isResumePreparedStudioVideoRequest(request)) {
const existing = turn.actions.find(
(action): action is AgentPrepareVideoAction => action.type === 'prepare_video',
Expand Down Expand Up @@ -2183,29 +2206,9 @@ export async function reconcileAgentTurnWithRequest(
actions: [rhythmic3dWorkflow],
}
}
if (isComicLaunchHowQuestion(request, history)) {
return {
reply: [
'No hay un botón llamado **Render page**.',
'El dibujo de las viñetas es **Generate all images** en Comic Director (barra de Comics), o dímelo aquí: **lánzalo**.',
'Las viñetas entran en la **misma GPU**, una detrás de otra, no en paralelo. No es un segundo motor.',
].join('\n\n'),
actions: [{ type: 'open_tab', tab: 'comics' }],
}
}
if (isLabsInventoryQuestion(request)) {
return { ...turn, actions: [] }
}
if (isHowToGenerateQuestion(request)) {
return {
...turn,
actions: turn.actions.filter(action => (
action.type === 'open_tab'
|| action.type === 'open_story_section'
|| action.type === 'open_series_section'
)),
}
}
if (!requestAuthorizesEditorialCommit(request)) {
const actions = turn.actions.filter(action => !EDITORIAL_COMMIT_TYPES.has(action.type))
if (actions.length !== turn.actions.length) turn = { ...turn, actions }
Expand Down
65 changes: 65 additions & 0 deletions ui/tests/agentActions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,71 @@ test('genera el video de un mapache uses the new topic instead of the prepared f
useStore.setState(original)
})

test('how-to launch questions do not queue a prepared Studio video', async () => {
const { useStore } = await import('../src/stores/useStore.ts')
const {
isHowToGenerateQuestion,
isResumePreparedStudioVideoRequest,
isExplicitVideoGenerationRequest,
isExplicitAudioGenerationRequest,
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: 'un mago en la torre' },
savedPromptPerMode: { ...useStore.getState().savedPromptPerMode, video: 'un mago en la torre' },
})

try {
for (const question of [
'How do I launch the video?',
'How do I start the video?',
'Explain how to launch the video',
]) {
assert.equal(isHowToGenerateQuestion(question), true, question)
assert.equal(isResumePreparedStudioVideoRequest(question), false, question)
assert.equal(isExplicitVideoGenerationRequest(question), false, question)
const turn = await reconcileAgentTurnWithRequest(question, {
reply: 'I will launch it.',
actions: [{ type: 'prepare_video', prompt: 'un mago en la torre' }, { type: 'start_generation', confirm: true }],
})
assert.deepEqual(turn.actions.map(action => action.type), [], question)
}

const longVideo = `How do I generate a video? ${'Please explain the workflow in detail. '.repeat(8)}`
assert.ok(longVideo.length > 240)
assert.equal(isHowToGenerateQuestion(longVideo), true)
assert.equal(isExplicitVideoGenerationRequest(longVideo), false)
const longVideoTurn = await reconcileAgentTurnWithRequest(longVideo, { reply: 'Here is how.', actions: [] })
assert.deepEqual(longVideoTurn.actions.map(action => action.type), [])

const longAudio = `How can I generate music in Studio Audio? ${'Please explain the workflow in detail. '.repeat(8)}`
assert.ok(longAudio.length > 240)
assert.equal(isHowToGenerateQuestion(longAudio), true)
assert.equal(isExplicitAudioGenerationRequest(longAudio), false)
const longAudioTurn = await reconcileAgentTurnWithRequest(longAudio, {
reply: 'I will generate it now.',
actions: [
{ type: 'open_tab', tab: 'audio' },
{ type: 'prepare_audio', subMode: 'music', prompt: 'Piano' },
{ type: 'start_generation', confirm: true },
],
})
assert.deepEqual(longAudioTurn.actions.map(action => action.type), ['open_tab'])

const launch = await reconcileAgentTurnWithRequest('lanza el vídeo', { reply: '¿De qué?', actions: [] })
assert.deepEqual(launch.actions.map(action => action.type), ['prepare_video', 'start_generation'])
assert.equal(launch.actions[0].prompt, 'un mago en la torre')
} finally {
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')
Expand Down
Loading