ai-chatbot/lib/video-agent/revise-storyboard-task.ts
Your Name ec988be30a
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Update agent workflow and video task handling
2026-03-13 11:21:42 +08:00

289 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import { generateCanvasImage } from "@/lib/canvas/generate-image";
import {
getVideoStoryboardTaskDetail,
markVideoStoryboardAwaitingConfirmation,
markVideoStoryboardDraftForRevision,
markVideoStoryboardHdCompleted,
markVideoStoryboardHdFailed,
markVideoStoryboardHdRunning,
syncVideoStoryboardHdStageStatus,
updateVideoStoryboardShotPrompt,
} from "@/lib/video-agent/task-store";
import {
interpretStoryboardCommand,
parseStoryboardRevisionCommand as parseStoryboardRevisionCommandWithContext,
type StoryboardRevisionCommand,
} from "@/lib/video-agent/storyboard-command-interpreter";
import type {
StoryboardGridType,
StoryboardProgressEvent,
} from "@/lib/video-agent/types";
function normalizeText(value: unknown) {
return String(value || "").trim();
}
function buildAwaitingConfirmationEvent(params: {
taskId: string;
detail: NonNullable<Awaited<ReturnType<typeof getVideoStoryboardTaskDetail>>>;
message: string;
}): StoryboardProgressEvent {
const task = params.detail.task as typeof params.detail.task & {
gridType?: StoryboardGridType | null;
gridRows?: number | null;
gridCols?: number | null;
};
return {
stage: "awaiting_confirmation",
provider: "ruxa-video",
taskId: params.taskId,
totalShots: Number(task.totalShots || params.detail.shots.length || 0),
gridType: task.gridType || undefined,
rows: Number(task.gridRows || 0) || undefined,
cols: Number(task.gridCols || 0) || undefined,
message: params.message,
shots: params.detail.shots.map((shot: any) => ({
shotIndex: Number(shot.shotIndex || 0),
tileImageUrl: normalizeText(shot.tileImageUrl),
previewImageUrl:
normalizeText(shot.hdImageUrl) ||
normalizeText(shot.upscaledImageUrl) ||
normalizeText(shot.tileImageUrl),
prompt: normalizeText(shot.prompt),
})),
};
}
function inferPreviewAspectRatio(gridType?: StoryboardGridType | null): string {
switch (gridType) {
case "grid_2x2":
case "grid_3x2":
case "grid_3x3":
default:
return "1:1";
}
}
export function parseStoryboardRevisionCommand(params: {
text: string;
totalShots?: number;
}): StoryboardRevisionCommand {
return parseStoryboardRevisionCommandWithContext(params);
}
export async function resolveStoryboardRevisionCommand(params: {
text: string;
detail: NonNullable<Awaited<ReturnType<typeof getVideoStoryboardTaskDetail>>>;
}) {
const interpreted = await interpretStoryboardCommand({
text: params.text,
totalShots: Number(params.detail.task.totalShots || params.detail.shots.length || 0),
items: params.detail.shots.map((shot: any) => ({
index: Number(shot.shotIndex || 0),
title: Number(shot.shotIndex || 0) > 0 ? `镜头 ${Number(shot.shotIndex || 0)}` : undefined,
summary: normalizeText(shot.prompt),
})),
});
return interpreted.revision;
}
export async function reviseStoryboardTaskByCommand(params: {
taskId: string;
command: StoryboardRevisionCommand;
onProgress?: (event: StoryboardProgressEvent) => void | Promise<void>;
}) {
if (!params.command) {
return {
ok: false,
message: "我收到了修改意图,但还没定位到具体镜头。请直接回复例如“修改第3镜头:……”或“重生第2张高清图”。",
detail: await getVideoStoryboardTaskDetail(params.taskId),
};
}
const detail = await getVideoStoryboardTaskDetail(params.taskId);
if (!detail) {
return {
ok: false,
message: "当前分镜任务不存在或已失效。",
detail: null,
};
}
const targetShot = detail.shots.find(
(shot: any) => Number(shot.shotIndex || 0) === params.command?.shotIndex
);
if (!targetShot) {
return {
ok: false,
message: `没有找到第 ${params.command.shotIndex} 镜头,请检查镜头编号后再试。`,
detail,
};
}
await markVideoStoryboardDraftForRevision({
taskId: params.taskId,
});
if (params.command.type === "update_prompt") {
await updateVideoStoryboardShotPrompt({
taskId: params.taskId,
shotIndex: params.command.shotIndex,
prompt: params.command.prompt,
});
}
const latestDetail = await getVideoStoryboardTaskDetail(params.taskId);
const latestShot = latestDetail?.shots.find(
(shot: any) => Number(shot.shotIndex || 0) === params.command?.shotIndex
);
const currentPrompt =
params.command.type === "update_prompt"
? params.command.prompt
: normalizeText(latestShot?.prompt) || normalizeText(targetShot.prompt);
const inputImageUrl =
normalizeText(latestShot?.upscaledImageUrl) ||
normalizeText(latestShot?.tileImageUrl) ||
normalizeText(targetShot.upscaledImageUrl) ||
normalizeText(targetShot.tileImageUrl);
if (!inputImageUrl) {
return {
ok: false,
message: `第 ${params.command.shotIndex} 镜头缺少输入图,无法重生高清预览。`,
detail: latestDetail || detail,
};
}
try {
await markVideoStoryboardHdRunning({
taskId: params.taskId,
shotIndex: params.command.shotIndex,
});
const hdResult = await generateCanvasImage({
prompt: currentPrompt,
referenceImage: inputImageUrl,
imageCount: 1,
imageSize: "2K",
aspectRatio: inferPreviewAspectRatio(
(latestDetail?.task as any)?.gridType || (detail.task as any)?.gridType
),
});
const previewImageUrl =
hdResult.images[0]?.originalUrl || hdResult.images[0]?.thumbnailUrl || inputImageUrl;
await markVideoStoryboardHdCompleted({
taskId: params.taskId,
shotIndex: params.command.shotIndex,
hdImageUrl: previewImageUrl,
hdImagePrompt: currentPrompt,
});
await syncVideoStoryboardHdStageStatus(params.taskId);
await markVideoStoryboardAwaitingConfirmation({
taskId: params.taskId,
});
await params.onProgress?.({
stage: "hd_shot_completed",
provider: "ruxa-video",
taskId: params.taskId,
shotIndex: params.command.shotIndex,
imageUrl: previewImageUrl,
prompt: currentPrompt,
});
const refreshedDetail = await getVideoStoryboardTaskDetail(params.taskId);
if (refreshedDetail) {
const message =
params.command.type === "update_prompt"
? `已更新第 ${params.command.shotIndex} 镜头,并重新生成了高清预览,请继续确认。`
: `已重生第 ${params.command.shotIndex} 张高清预览,请继续确认。`;
await params.onProgress?.(
buildAwaitingConfirmationEvent({
taskId: params.taskId,
detail: refreshedDetail,
message,
})
);
return {
ok: true,
message,
detail: refreshedDetail,
};
}
return {
ok: true,
message:
params.command.type === "update_prompt"
? `已更新第 ${params.command.shotIndex} 镜头,并重新生成高清预览。`
: `已重生第 ${params.command.shotIndex} 张高清预览。`,
detail: latestDetail || detail,
};
} catch (error) {
const errorMessage =
normalizeText(error instanceof Error ? error.message : String(error)) ||
"高清预览重生失败";
await markVideoStoryboardHdFailed({
taskId: params.taskId,
shotIndex: params.command.shotIndex,
errorMessage,
});
await params.onProgress?.({
stage: "hd_shot_failed",
provider: "ruxa-video",
taskId: params.taskId,
shotIndex: params.command.shotIndex,
error: errorMessage,
});
return {
ok: false,
message: `第 ${params.command.shotIndex} 镜头的高清预览重生失败:${errorMessage}`,
detail: await getVideoStoryboardTaskDetail(params.taskId),
};
}
}
export async function updateStoryboardShotPromptAndRegenerateHd(params: {
taskId: string;
shotIndex: number;
prompt: string;
onProgress?: (event: StoryboardProgressEvent) => void | Promise<void>;
}) {
return reviseStoryboardTaskByCommand({
taskId: params.taskId,
command: {
type: "update_prompt",
shotIndex: params.shotIndex,
prompt: params.prompt,
},
onProgress: params.onProgress,
});
}
export async function regenerateStoryboardShotHd(params: {
taskId: string;
shotIndex: number;
onProgress?: (event: StoryboardProgressEvent) => void | Promise<void>;
}) {
return reviseStoryboardTaskByCommand({
taskId: params.taskId,
command: {
type: "regenerate_hd",
shotIndex: params.shotIndex,
},
onProgress: params.onProgress,
});
}