feat(canvas): 生成合同 - 统一节点参数并补充模型上下文能力
This commit is contained in:
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { canvasConnectionError } from "../src/lib/canvas/canvas-connection-policy";
|
||||
import { assertCanvasImageReferenceLimit, buildGenerationConfig, canvasImageReferenceLimitError, resolveCanvasGenerationModel } from "../src/lib/canvas/canvas-project-generation";
|
||||
import { readNodeGenerationSpec, synchronizeGenerationSpec } from "../src/lib/canvas/generation-contract";
|
||||
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
|
||||
import { groupModelsByDisplayName, inferVideoOperation, modelCompatibilityError, modelGroupReferenceLimits, modelPromptLengthError, resolveCompatibleModel, resolveModelGenerationDefaults, resolveModelVideoBooleanOptions } from "../src/lib/model-selection";
|
||||
import { defaultConfig, normalizeModelOptionValue, type AiConfig, type ModelChannel } from "../src/stores/use-config-store";
|
||||
@@ -53,6 +54,87 @@ function node(id: string, type: CanvasNodeType, generationMode?: "image" | "vide
|
||||
}
|
||||
|
||||
describe("逻辑模型选择", () => {
|
||||
|
||||
test("generationSpec 的显式 false 和 0 覆盖陈旧镜像及全局默认", () => {
|
||||
const config = policyConfig();
|
||||
config.videoSeconds = "9";
|
||||
config.videoGenerateAudio = "true";
|
||||
config.videoWatermark = "true";
|
||||
const videoNode: CanvasNodeData = {
|
||||
...node("video", CanvasNodeType.Video),
|
||||
metadata: {
|
||||
model: "relay::cinema-image",
|
||||
seconds: "15",
|
||||
generateAudio: "true",
|
||||
watermark: "true",
|
||||
generationSpec: {
|
||||
version: 1,
|
||||
mode: "video",
|
||||
prompt: "canonical prompt",
|
||||
modelSelection: { kind: "channel", channelId: "relay", modelKey: "cinema-text" },
|
||||
options: { durationSeconds: 0, generateAudio: false, watermark: false },
|
||||
referenceBindings: [],
|
||||
textInputMode: "prompt-only",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const generation = buildGenerationConfig(config, videoNode, "video");
|
||||
|
||||
expect(generation.model).toBe("relay::cinema-text");
|
||||
expect(generation.videoSeconds).toBe("1");
|
||||
expect(generation.videoGenerateAudio).toBe("false");
|
||||
expect(generation.videoWatermark).toBe("false");
|
||||
});
|
||||
|
||||
test("编辑兼容字段时原子更新 canonical 合同和全部镜像", () => {
|
||||
const original: CanvasNodeData = {
|
||||
...node("video", CanvasNodeType.Video),
|
||||
metadata: {
|
||||
seconds: "6",
|
||||
generateAudio: "false",
|
||||
generationSpec: {
|
||||
version: 1,
|
||||
mode: "video",
|
||||
prompt: "before",
|
||||
options: { durationSeconds: 6, generateAudio: false },
|
||||
referenceBindings: [],
|
||||
textInputMode: "prompt-only",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const edited = synchronizeGenerationSpec(original, { seconds: "9", generateAudio: "true", composerContent: "after" });
|
||||
const spec = readNodeGenerationSpec(edited);
|
||||
|
||||
expect(spec?.prompt).toBe("after");
|
||||
expect(spec?.options.durationSeconds).toBe(9);
|
||||
expect(spec?.options.generateAudio).toBe(true);
|
||||
expect(edited.metadata?.seconds).toBe("9");
|
||||
expect(edited.metadata?.generateAudio).toBe("true");
|
||||
});
|
||||
|
||||
test("逻辑模型合同不会被旧 metadata.model 覆盖", () => {
|
||||
const config = policyConfig();
|
||||
config.channels[0]!.modelCosts![0]!.logicalModelId = "cinema-logical";
|
||||
const videoNode: CanvasNodeData = {
|
||||
...node("video", CanvasNodeType.Video),
|
||||
metadata: {
|
||||
model: "relay::cinema-image",
|
||||
generationSpec: {
|
||||
version: 1,
|
||||
mode: "video",
|
||||
prompt: "logical selection",
|
||||
modelSelection: { kind: "logical", logicalModelId: "cinema-logical" },
|
||||
options: {},
|
||||
referenceBindings: [],
|
||||
textInputMode: "prompt-only",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(buildGenerationConfig(config, videoNode, "video").model).toBe("relay::cinema-text");
|
||||
});
|
||||
test("技能上下文展开后按最终视频模型字符上限拒绝提交", () => {
|
||||
const config = policyConfig();
|
||||
const model = config.videoModels[0]!;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { buildNodeGenerationContext } from "../src/components/canvas/canvas-node-generation";
|
||||
import { buildCanvasResourceReferences } from "../src/lib/canvas/canvas-resource-references";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../src/types/canvas";
|
||||
|
||||
function node(id: string, type: CanvasNodeType, content: string): CanvasNodeData {
|
||||
@@ -32,6 +33,28 @@ function connection(fromNodeId: string): CanvasConnection {
|
||||
}
|
||||
|
||||
describe("canvas node generation position mentions", () => {
|
||||
test("Agent 保存的素材引用块在编辑器和再次提交时保持相同编号", () => {
|
||||
const target = targetNode();
|
||||
const character = node("character", CanvasNodeType.Image, "data:image/png;base64,a");
|
||||
const wig = node("wig", CanvasNodeType.Image, "data:image/png;base64,b");
|
||||
const voice = node("voice", CanvasNodeType.Audio, "data:audio/mpeg;base64,c");
|
||||
const note = node("note", CanvasNodeType.Markdown, "导演方案");
|
||||
const prompt = "一镜到底\n\n【资产参考】\n人物:@图片1\n假发:@图片2\n声音:@音频1";
|
||||
target.metadata = { composerContent: prompt, prompt, referenceNodeIds: [character.id, voice.id, wig.id] };
|
||||
const nodes = [target, wig, voice, note, character];
|
||||
const connections = [character, voice, wig, note].map((source) => connection(source.id));
|
||||
const references = buildCanvasResourceReferences(nodes, connections, target.id).filter((reference) => reference.active);
|
||||
|
||||
expect(Object.fromEntries(references.map((reference) => [reference.nodeId, reference.label]))).toEqual({
|
||||
character: "图片1", wig: "图片2", voice: "音频1", note: "文本1",
|
||||
});
|
||||
const context = buildNodeGenerationContext(target.id, nodes, connections, prompt, [], true);
|
||||
expect(context.referenceImages.map((reference) => reference.id)).toEqual([character.id, wig.id]);
|
||||
expect(context.referenceAudios.map((reference) => reference.id)).toEqual([voice.id]);
|
||||
expect(context.prompt).toBe(prompt);
|
||||
expect(context.textCount).toBe(0);
|
||||
});
|
||||
|
||||
test("已有图片节点显式引用自身时作为图生图参考图提交", () => {
|
||||
const source = node("image-self", CanvasNodeType.Image, "data:image/png;base64,a");
|
||||
source.metadata.composerContent = "将 @图片1 图片变清晰";
|
||||
|
||||
Reference in New Issue
Block a user