148 lines
7.1 KiB
TypeScript
148 lines
7.1 KiB
TypeScript
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("production workbench does not silently drop bound voice samples before backend validation", async () => {
|
|
const source = await Bun.file(new URL("../src/pages/projects/detail/workflow-production-workbench.tsx", import.meta.url)).text();
|
|
|
|
expect(source).toContain('const generationReferenceAudios = generationCapability === "video" ? shotAssetReferenceContext.referenceAudios : [];');
|
|
expect(source).not.toContain("selectedVideoProfile?.references.maxAudios");
|
|
});
|
|
|
|
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);
|
|
});
|