54 lines
3.6 KiB
TypeScript
54 lines
3.6 KiB
TypeScript
import type { CanvasNodeData } from "@/types/canvas";
|
||
|
||
export type StoryboardGenerationContext = {
|
||
characters: Array<{
|
||
assetId: string;
|
||
versionId: string;
|
||
name: string;
|
||
definition: Record<string, unknown>;
|
||
}>;
|
||
};
|
||
|
||
// 分镜的两条入口共用角色资产校验,避免角色版本在某条旁路里被遗漏。
|
||
export function resolveStoryboardGenerationContext(nodes: CanvasNodeData[]): StoryboardGenerationContext {
|
||
// `workflowKind=character` is also used by standalone character-design
|
||
// image workflows. Only nodes linked to a project character asset are
|
||
// storyboard character cards and therefore participate in this check.
|
||
const characterNodes = nodes.filter((node) => node.metadata?.workflowKind === "character" && node.metadata?.characterAssetId?.trim());
|
||
const invalidCharacter = characterNodes.find((node) => !node.metadata?.characterAssetId?.trim() || !node.metadata?.characterVersionId?.trim() || !(node.metadata?.characterName || node.title).trim());
|
||
if (invalidCharacter) throw new Error(`角色卡“${invalidCharacter.metadata?.characterName || invalidCharacter.title || "未命名角色"}”版本未同步,请刷新角色资产后再生成分镜`);
|
||
|
||
return {
|
||
characters: characterNodes.map((node) => ({
|
||
assetId: node.metadata!.characterAssetId!.trim(),
|
||
versionId: node.metadata!.characterVersionId!.trim(),
|
||
name: (node.metadata?.characterName || node.title).trim(),
|
||
definition: node.metadata?.characterDefinition || { prompt: node.metadata?.characterPrompt || "" },
|
||
})),
|
||
};
|
||
}
|
||
|
||
// Expose the same generation prerequisites to the Agent before it starts a workflow.
|
||
export function inspectStoryboardReadiness(nodes: CanvasNodeData[]) {
|
||
let generationContext: StoryboardGenerationContext | undefined;
|
||
let blockingReason: string | undefined;
|
||
try { generationContext = resolveStoryboardGenerationContext(nodes); }
|
||
catch (error) { blockingReason = error instanceof Error ? error.message : "分镜上下文未就绪"; }
|
||
const styleNode = nodes.find((node) => node.metadata?.workflowKind === "styleboard");
|
||
const styleReady = Boolean(styleNode?.metadata?.stylePresetId?.trim() && String(styleNode.metadata.content || styleNode.metadata.prompt || "").trim());
|
||
const storyboards = nodes.filter((node) => node.type === "script").map((node) => {
|
||
const rows = node.metadata?.storyboard?.rows || [];
|
||
const incompleteRowIds = rows.filter((row) => !Number.isFinite(row.durationSeconds) || row.durationSeconds <= 0 || !row.videoMotionPrompt?.trim()).map((row) => row.id);
|
||
return { nodeId: node.id, title: node.title, rowCount: rows.length, populated: rows.length > 0 && incompleteRowIds.length === 0, incompleteRowIds, totalDurationSeconds: rows.reduce((total, row) => total + (Number.isFinite(row.durationSeconds) && row.durationSeconds > 0 ? row.durationSeconds : 0), 0) };
|
||
});
|
||
return {
|
||
canGenerateStoryboard: Boolean(generationContext), blockingReason,
|
||
style: { ready: styleReady, nodeId: styleNode?.id, presetId: styleNode?.metadata?.stylePresetId, title: styleNode?.title },
|
||
storyboards,
|
||
nextActions: [
|
||
...(!styleReady ? ["先 canvas_list_styles 读取真实画风,结合已确认风格调用 canvas_apply_style;该工具会复用或创建画风节点。仅在缺少关键风格取舍时询问用户。"] : []),
|
||
...(storyboards.some((item) => !item.populated) ? ["先 canvas_read_storyboard 读取真实行 ID,再 canvas_edit_storyboard 更新空行并按需新增镜头;正文或标题不代表已填充分镜表。"] : []),
|
||
],
|
||
};
|
||
}
|