Files
ttstd-infinite-canvas/web/test/workflow-shot-generation.test.ts

141 lines
6.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 { expect, test } from "bun:test";
import { defaultConfig } from "../src/stores/use-config-store";
import { submitBackendGenerationTask, type GenerationTaskDependencies } from "../src/services/api/generation-task";
import type { GenerationTask } from "../src/services/api/task-center";
import type { ProjectDetail } from "../src/services/api/projects";
import { buildShotAssetReferenceContext, resolveShotAssetMentionPrompt } from "../src/pages/projects/detail/workflow-shot-references";
test("shot generation submits historical character image, current voice and asset prompt", async () => {
const detail = {
assets: [
{
id: "character-1",
title: "张天昊",
category: "character",
mediaType: "image",
primaryVersionId: "character-version-2",
character: {
versionId: "character-version-2",
definition: { voiceLanguage: "普通话", voiceAge: "青年男性", voiceTimbre: "略带疲惫和震惊" },
representations: [{ id: "representation-1", resourceId: "character-image-1", mediaType: "image/png", role: "primary" }],
voice: { profile: { sampleResourceId: "character-audio-1", language: "普通话", timbre: "沉稳" }, instructions: "内心独白语气" },
},
},
{
id: "scene-1",
title: "坑底场景",
category: "environment",
mediaType: "image",
primaryVersionId: "scene-version-1",
storageKey: "resource:scene-image-1",
},
],
shotReferences: [
{
shotId: "shot-1",
assetVersionId: "character-version-1",
status: "linked",
asset: {
id: "character-1",
title: "张天昊",
category: "character",
mediaType: "entity",
primaryVersionId: "character-version-2",
character: {
versionId: "character-version-2",
definition: { voiceLanguage: "普通话", voiceAge: "青年男性", voiceTimbre: "略带疲惫和震惊" },
representations: [{ id: "representation-2", resourceId: "character-image-2", mediaType: "image/png", role: "primary" }],
voice: { profile: { sampleResourceId: "character-audio-1", language: "普通话", timbre: "沉稳" }, instructions: "内心独白语气" },
},
},
referencedVersion: {
id: "character-version-1",
assetId: "character-1",
version: 1,
representations: [{ id: "representation-1", resourceId: "character-image-1", mediaType: "image/png", role: "primary" }],
},
},
{ shotId: "shot-1", assetVersionId: "scene-version-1", status: "linked" },
],
} as ProjectDetail;
const context = buildShotAssetReferenceContext(detail, "shot-1");
const prompt = resolveShotAssetMentionPrompt("张天昊在 @[asset:scene-1] 睁开眼睛", context, { dialogue: "我穿越了?瓦西国张家废柴少主?" });
expect(context.referenceImages).toHaveLength(2);
expect(context.referenceImages[0]?.storageKey).toBe("resource:character-image-1");
expect(context.referenceAudios).toHaveLength(1);
expect(context.referenceAudios[0]?.storageKey).toBe("resource:character-audio-1");
expect(context.resolvedCharacterVersions).toEqual([{ assetId: "character-1", versionId: "character-version-1" }]);
expect(prompt).toContain("张天昊在 图片2 睁开眼睛");
expect(prompt).toContain("- 张天昊:人物参考:图片1;声音参考:音频1");
expect(prompt).toContain("声音画像:普通话;青年男性;略带疲惫和震惊;内心独白语气");
expect(prompt).toContain("镜头台词:我穿越了?瓦西国张家废柴少主?");
expect(prompt).toContain("- 坑底场景:场景参考:图片2");
let createdInput: Parameters<GenerationTaskDependencies["createTask"]>[0] | undefined;
const task = {
id: "shot-task-1",
type: "canvas_video",
status: "queued",
prompt,
attempts: 0,
createdAt: "2026-08-30T00:00:00.000Z",
updatedAt: "2026-08-30T00:00:00.000Z",
} satisfies GenerationTask;
await submitBackendGenerationTask({
projectId: "project-1",
mode: "video",
prompt,
config: { ...defaultConfig, model: "MiniMax-H3", videoModel: "MiniMax-H3" },
referenceImages: context.referenceImages,
referenceAudios: context.referenceAudios,
metadata: { shotId: "shot-1", videoEditOperation: "reference_to_video" },
}, {
createTask: async (input) => { createdInput = input; return task; },
waitTask: async () => { throw new Error("should not wait"); },
runLocal: async () => ({ mode: "video" }),
createId: () => "id-1",
now: () => "2026-08-30T00:00:00.000Z",
});
expect(createdInput?.prompt).toContain("【资产参考】");
expect(createdInput?.input.referenceImages.map((reference) => reference.storageKey)).toEqual(["resource:character-image-1", "resource:scene-image-1"]);
expect(createdInput?.input.referenceAudios.map((reference) => reference.storageKey)).toEqual(["resource:character-audio-1"]);
});
test("background generation submission returns after task creation without waiting", async () => {
let waitCalls = 0;
const task = {
id: "task-1",
type: "canvas_video",
status: "queued",
prompt: "角色表演",
attempts: 0,
createdAt: "2026-08-30T00:00:00.000Z",
updatedAt: "2026-08-30T00:00:00.000Z",
} satisfies GenerationTask;
const dependencies: GenerationTaskDependencies = {
createTask: async () => task,
waitTask: async () => {
waitCalls += 1;
throw new Error("should not wait");
},
runLocal: async () => ({ mode: "video" }),
createId: () => "id-1",
now: () => "2026-08-30T00:00:00.000Z",
};
const submitted = await submitBackendGenerationTask({
projectId: "project-1",
mode: "video",
prompt: "角色表演",
config: { ...defaultConfig, model: "MiniMax-H3", videoModel: "MiniMax-H3" },
metadata: { shotId: "shot-1", videoEditOperation: "reference_to_video" },
}, dependencies);
expect(submitted).toBe(task);
expect(waitCalls).toBe(0);
});