diff --git a/ui/src/features/agent/agentActions.ts b/ui/src/features/agent/agentActions.ts index 76b72884..fc90096e 100644 --- a/ui/src/features/agent/agentActions.ts +++ b/ui/src/features/agent/agentActions.ts @@ -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, ) @@ -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)) } @@ -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)) } @@ -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)) } @@ -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 @@ -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', @@ -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 } diff --git a/ui/tests/agentActions.test.mjs b/ui/tests/agentActions.test.mjs index 20036fa4..a37727af 100644 --- a/ui/tests/agentActions.test.mjs +++ b/ui/tests/agentActions.test.mjs @@ -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')