import { expect, test } from "@playwright/test"; import { defaultVideoCanvasFunctionConfig } from "../../lib/video-canvas/config"; import { addVideoCanvasAttachedSubject, buildVideoCanvasSubjectProviderReferences, composeVideoCanvasSubjectPrompt, getVideoCanvasAttachedSubjects, normalizeGeneratedSubjectDescription, removeVideoCanvasAttachedSubject, resolveVideoCanvasSubjectCategory, supportsVideoCanvasSubjectUsage, toVideoCanvasAttachedSubject, validateVideoCanvasSubjectDraft, } from "../../lib/video-canvas/subjects"; const config = defaultVideoCanvasFunctionConfig.subjectLibrary; test.describe("video canvas subject library", () => { test("validates the exact image and video creation ranges", () => { expect( validateVideoCanvasSubjectDraft( { assetCount: 1, category: "人物", description: "古风女性人物,黑色长发,绿色衣裙。", mediaMode: "images", name: "古风女主", voiceId: "none", }, config, ), ).toBe("请至少添加2张角色图片"); expect( validateVideoCanvasSubjectDraft( { assetCount: 2, category: "人物", description: "古风女性人物,黑色长发,绿色衣裙。", mediaMode: "images", name: "古风女主", voiceId: "gentle-female", }, config, ), ).toBeNull(); expect( validateVideoCanvasSubjectDraft( { assetCount: 1, category: "人物", description: "古风女性人物,黑色长发,绿色衣裙。", durationSeconds: 8.1, mediaMode: "video", name: "古风女主", voiceId: "original-video", }, config, ), ).toBe("角色视频时长需为3-8秒"); }); test("normalizes labels and smart descriptions", () => { expect(resolveVideoCanvasSubjectCategory("场景", config)).toBe("场景"); expect(resolveVideoCanvasSubjectCategory("unknown", config)).toBe("其他"); expect(resolveVideoCanvasSubjectCategory("", config)).toBe("其他"); expect( normalizeGeneratedSubjectDescription( "```text\n主体描述:一位黑色长发、绿色衣裙的古风女性人物。\n```", 100, ), ).toBe("一位黑色长发、绿色衣裙的古风女性人物。"); }); test("accepts uploaded custom voice ids", () => { expect( validateVideoCanvasSubjectDraft( { assetCount: 2, category: "人物", description: "古风女性人物,黑色长发,绿色衣裙。", mediaMode: "images", name: "古风女主", voiceId: "custom:25c7404b-80ae-4f11-a237-a6ec35c4ed6a", }, config, ), ).toBeNull(); }); test("stores subject references without changing unrelated node draft values", () => { const subject = toVideoCanvasAttachedSubject({ description: { description: "成年古装男性,黑发束冠,金黄色龙纹长袍。" }, displayName: "古装男性", id: "subject-1", metadata: { category: "人物", mediaMode: "images", mediaUrls: ["https://example.com/front.png", "https://example.com/back.png"], voiceId: "none", voiceName: "无配音", }, thumbnailUrl: "https://example.com/front.png", }); expect(subject).not.toBeNull(); const attached = addVideoCanvasAttachedSubject( { params: { model: "kling-o3", prompt: "人物转身。" }, untouched: true }, subject!, 4, ); expect(getVideoCanvasAttachedSubjects(attached)).toEqual([subject]); expect(attached).toMatchObject({ params: { model: "kling-o3", prompt: "人物转身。", subjects: [subject] }, untouched: true, videoSubjects: [subject], }); const removed = removeVideoCanvasAttachedSubject(attached, "subject-1"); expect(getVideoCanvasAttachedSubjects(removed)).toEqual([]); expect(removed).toMatchObject({ params: { model: "kling-o3", prompt: "人物转身。" }, untouched: true, }); }); test("maps subject data to hidden prompt and structured provider references", () => { const subject = toVideoCanvasAttachedSubject({ description: { description: "白色短发,黑色机能服。" }, displayName: "测试主体", id: "subject-2", metadata: { category: "人物", mediaMode: "images", mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"], voiceId: "broadcast-male", voiceName: "播报男声", }, originalUrl: "https://example.com/a.png", })!; const prompt = composeVideoCanvasSubjectPrompt( "主体走向镜头。", [subject], "主体{{index}}「{{name}}」({{category}}):{{description}},音色{{voiceName}}。", ); expect(prompt).toBe( "主体走向镜头。 主体1「测试主体」(人物):白色短发,黑色机能服。,音色播报男声。", ); expect(buildVideoCanvasSubjectProviderReferences([subject])).toEqual([ { category: "人物", description: "白色短发,黑色机能服。", name: "测试主体", referenceImages: ["https://example.com/a.png", "https://example.com/b.png"], referenceVideo: undefined, subjectId: "subject-2", voiceId: "broadcast-male", }, ]); }); test("shows subject usage only for administrator-configured video models", () => { expect(supportsVideoCanvasSubjectUsage(config, "Kling O3")).toBe(true); expect(supportsVideoCanvasSubjectUsage(config, "Seedance 2.0 VIP")).toBe(false); expect( supportsVideoCanvasSubjectUsage( { ...config, videoUsage: { ...config.videoUsage, enabled: false } }, "Kling O3", ), ).toBe(false); }); });