Files
ttstd-infinite-canvas/web/src/lib/canvas/canvas-storyboard-context.ts

54 lines
3.6 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 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 更新空行并按需新增镜头;正文或标题不代表已填充分镜表。"] : []),
],
};
}