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

83 lines
4.6 KiB
TypeScript

import { nanoid } from "nanoid";
import { storyboardAssetRoleForNode } from "@/lib/canvas/canvas-storyboard-assets";
import { buildOrderedCanvasResourceReferences, canvasResourceMentionToken } from "@/lib/canvas/canvas-resource-references";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type StoryboardAssetBinding, type StoryboardRow } from "@/types/canvas";
export function storyboardRowReferenceNodeIds(
scriptNode: CanvasNodeData,
row: StoryboardRow,
nodes: CanvasNodeData[],
connections: CanvasConnection[],
includeFirstFrame: boolean,
targetNodeId?: string,
) {
const characterAssetIds = new Set((row.characters || []).map((character) => character.characterAssetId).filter((assetId): assetId is string => Boolean(assetId)));
const characterNodeIds = nodes
.filter((node) => node.metadata?.workflowKind === "character" && Boolean(node.metadata.characterAssetId) && characterAssetIds.has(node.metadata.characterAssetId!))
.map((node) => node.id);
const referenceIds = new Set([
...(scriptNode.metadata?.storyboard?.referenceNodeIds || []),
...(row.assetBindings || []).map((binding) => binding.nodeId),
...characterNodeIds,
...connections.filter((connection) => connection.toNodeId === scriptNode.id && connection.toHandleId === `row:${row.id}`).map((connection) => connection.fromNodeId),
...(targetNodeId ? connections.filter((connection) => !connection.relation && connection.toNodeId === targetNodeId).map((connection) => connection.fromNodeId) : []),
...(includeFirstFrame && row.imageNodeId ? [row.imageNodeId] : []),
]);
if (!includeFirstFrame && row.imageNodeId) referenceIds.delete(row.imageNodeId);
referenceIds.delete(scriptNode.id);
return Array.from(referenceIds).filter((nodeId) => nodes.some((node) => node.id === nodeId));
}
export function storyboardComposerContent(prompt: string, referenceNodeIds: string[], nodes: CanvasNodeData[]) {
const referenceIds = Array.from(new Set(referenceNodeIds));
const referenceNodes = referenceIds.flatMap((nodeId) => {
const node = nodes.find((candidate) => candidate.id === nodeId);
return node ? [node] : [];
});
const mentions = buildOrderedCanvasResourceReferences(referenceNodes).map(canvasResourceMentionToken);
return mentions.length ? [`参考资产:${mentions.join(" ")}`, prompt.trim()].filter(Boolean).join("\n") : prompt.trim();
}
export function reconcileStoryboardTargetConnections(
connections: CanvasConnection[],
scriptNode: CanvasNodeData,
row: StoryboardRow,
targetNodeId: string,
referenceNodeIds: string[],
) {
const desired = new Set(referenceNodeIds.filter((nodeId) => nodeId !== targetNodeId && nodeId !== scriptNode.id));
const next = connections.filter((connection) => {
if (connection.storyboardRowId !== row.id || connection.toNodeId !== targetNodeId) return true;
if (connection.relation === "storyboard-output") return connection.fromNodeId === scriptNode.id;
if (connection.relation === "storyboard-asset-reference") return desired.has(connection.fromNodeId);
return true;
});
if (!next.some((connection) => connection.fromNodeId === scriptNode.id && connection.toNodeId === targetNodeId && connection.fromHandleId === `row:${row.id}`)) {
next.push({ id: nanoid(), fromNodeId: scriptNode.id, toNodeId: targetNodeId, fromHandleId: `row:${row.id}`, relation: "storyboard-output", storyboardRowId: row.id });
}
desired.forEach((fromNodeId) => {
if (!next.some((connection) => connection.fromNodeId === fromNodeId && connection.toNodeId === targetNodeId)) {
next.push({ id: nanoid(), fromNodeId, toNodeId: targetNodeId, relation: "storyboard-asset-reference", storyboardRowId: row.id });
}
});
return next;
}
export function bindingForConnectedNode(node: CanvasNodeData): StoryboardAssetBinding | null {
const role = storyboardAssetRoleForNode(node);
return role ? { nodeId: node.id, role, priority: defaultRolePriority(role) } : null;
}
export function isStoryboardPreviewAsset(node: CanvasNodeData) {
return node.type === CanvasNodeType.Image || node.type === CanvasNodeType.Drawing || node.type === CanvasNodeType.Video || node.type === CanvasNodeType.Audio || node.metadata?.workflowKind === "character";
}
function defaultRolePriority(role: StoryboardAssetBinding["role"]) {
if (role === "character") return 100;
if (role === "environment") return 90;
if (role === "prop" || role === "weapon" || role === "wardrobe") return 80;
if (role === "motion" || role === "audio") return 70;
return 60;
}