90 lines
4.8 KiB
TypeScript
90 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
|
|
import { buildStoryboardAssetCatalog } from "@/lib/canvas/canvas-storyboard-assets";
|
|
import { reconcileStoryboardTargetConnections, storyboardComposerContent, storyboardRowReferenceNodeIds } from "@/lib/canvas/canvas-storyboard-materializer";
|
|
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type StoryboardRow } from "@/types/canvas";
|
|
|
|
const node = (id: string, type: CanvasNodeType, metadata: CanvasNodeData["metadata"] = {}): CanvasNodeData => ({
|
|
id,
|
|
type,
|
|
title: id,
|
|
position: { x: 0, y: 0 },
|
|
width: 320,
|
|
height: 180,
|
|
metadata,
|
|
});
|
|
|
|
const row: StoryboardRow = {
|
|
id: "row-1",
|
|
shotNumber: 1,
|
|
durationSeconds: 6,
|
|
plotDescription: "雨夜追逐",
|
|
dialogue: "快走",
|
|
characters: [{ characterName: "林岚", characterAssetId: "character-asset" }],
|
|
narrativeIntent: "",
|
|
viewerPOV: "",
|
|
performanceBlocking: "",
|
|
shotSize: "",
|
|
emotion: "",
|
|
lightingAndAtmosphere: "",
|
|
audioEffects: "",
|
|
camera: "",
|
|
motion: "",
|
|
timeBeats: "",
|
|
imageGenerationPrompt: "",
|
|
videoMotionPrompt: "快速跟拍",
|
|
mustHave: [],
|
|
optionalDetails: [],
|
|
continuityOut: "",
|
|
negativePrompt: "",
|
|
assetBindings: [{ nodeId: "prop", role: "prop", priority: 80 }],
|
|
imageNodeId: "first-frame",
|
|
status: "idle",
|
|
};
|
|
|
|
describe("storyboard asset catalog", () => {
|
|
it("sends reusable image, video, audio and character assets but excludes generated shots", () => {
|
|
const assets = buildStoryboardAssetCatalog([
|
|
node("image", CanvasNodeType.Image, { content: "data:image/png;base64,x", assetCategory: "environment" }),
|
|
node("video", CanvasNodeType.Video, { storageKey: "resource:video" }),
|
|
node("audio", CanvasNodeType.Audio, { content: "data:audio/wav;base64,x" }),
|
|
node("character", CanvasNodeType.Image, { workflowKind: "character", characterAssetId: "character-asset", characterVersionId: "v1" }),
|
|
node("shot", CanvasNodeType.Image, { content: "data:image/png;base64,x", workflowKind: "shot" }),
|
|
node("text", CanvasNodeType.Text, { content: "story" }),
|
|
]);
|
|
|
|
expect(assets.map((asset) => [asset.id, asset.type])).toEqual([
|
|
["image", "image"],
|
|
["video", "video"],
|
|
["audio", "audio"],
|
|
["character", "character"],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("storyboard target materializer", () => {
|
|
const script = node("script", CanvasNodeType.Script, { storyboard: { rows: [row], visibleColumns: [], referenceNodeIds: ["project-style"] } });
|
|
const nodes = [script, node("project-style", CanvasNodeType.Image, { content: "style" }), node("prop", CanvasNodeType.Image, { content: "prop" }), node("character", CanvasNodeType.Image, { workflowKind: "character", characterAssetId: "character-asset" }), node("manual", CanvasNodeType.Video, { content: "video" }), node("direct-manual", CanvasNodeType.Audio, { content: "audio" }), node("first-frame", CanvasNodeType.Image, { content: "frame", workflowKind: "shot" }), node("target", CanvasNodeType.Video)];
|
|
const connections: CanvasConnection[] = [{ id: "manual-row-input", fromNodeId: "manual", toNodeId: "script", toHandleId: "row:row-1" }];
|
|
|
|
it("combines stable bindings and produces position mention tokens", () => {
|
|
const references = storyboardRowReferenceNodeIds(script, row, nodes, connections, true);
|
|
expect(references).toEqual(["project-style", "prop", "character", "manual", "first-frame"]);
|
|
expect(storyboardComposerContent("快速跟拍", references, nodes)).toBe("参考资产:@图片1 @图片2 @角色3 @视频1 @图片4\n快速跟拍");
|
|
|
|
const withDirectManualInput = storyboardRowReferenceNodeIds(script, row, nodes, [...connections, { id: "direct", fromNodeId: "direct-manual", toNodeId: "target" }], false, "target");
|
|
expect(withDirectManualInput).toContain("direct-manual");
|
|
});
|
|
|
|
it("reconciles managed edges without deleting manual connections", () => {
|
|
const manualTargetEdge: CanvasConnection = { id: "manual-target", fromNodeId: "manual", toNodeId: "target" };
|
|
const created = reconcileStoryboardTargetConnections([manualTargetEdge], script, row, "target", ["prop", "character"]);
|
|
expect(created.filter((edge) => edge.relation === "storyboard-output")).toHaveLength(1);
|
|
expect(created.filter((edge) => edge.relation === "storyboard-asset-reference").map((edge) => edge.fromNodeId).sort()).toEqual(["character", "prop"]);
|
|
|
|
const reconciled = reconcileStoryboardTargetConnections(created, script, row, "target", ["character"]);
|
|
expect(reconciled.some((edge) => edge.id === "manual-target")).toBe(true);
|
|
expect(reconciled.some((edge) => edge.relation === "storyboard-asset-reference" && edge.fromNodeId === "prop")).toBe(false);
|
|
});
|
|
});
|