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>>; 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>>; }) { 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; }) { 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; }) { 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; }) { return reviseStoryboardTaskByCommand({ taskId: params.taskId, command: { type: "regenerate_hd", shotIndex: params.shotIndex, }, onProgress: params.onProgress, }); }