From b315d2c06001851f607b649b58acd47b21ed4384 Mon Sep 17 00:00:00 2001 From: yun Date: Sat, 4 Jul 2026 12:15:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E7=89=87=E9=A1=BA=E5=BA=8F=E6=94=B9?= =?UTF-8?q?=E5=8F=98=E9=97=AE=E9=A2=98=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../providers/__tests__/popiserver.test.ts | 175 ++++++++---------- src/utils/popiserverTaskPayload.ts | 99 +--------- 2 files changed, 78 insertions(+), 196 deletions(-) diff --git a/src/app/api/generate/providers/__tests__/popiserver.test.ts b/src/app/api/generate/providers/__tests__/popiserver.test.ts index 9fed0e2e..c2fed7df 100644 --- a/src/app/api/generate/providers/__tests__/popiserver.test.ts +++ b/src/app/api/generate/providers/__tests__/popiserver.test.ts @@ -35,6 +35,10 @@ function makeInput(): GenerationInput { }, prompt: "@角色1 和 @角色2 在一起吃火锅", images: ["https://example.com/1.png", "https://example.com/2.png"], + referenceSubjectList: [ + { id: 1, type: "image", url: "https://example.com/1.png", name: "图1" }, + { id: 2, type: "image", url: "https://example.com/2.png", name: "图2" }, + ], subType: POPI_VIDEO_SUBTYPE_OMNI_REFERENCE, parameters: { ratio: "16:9", @@ -97,15 +101,11 @@ describe("popiserver generation provider", () => { expect(body).toMatchObject({ type: 2, subType: POPI_VIDEO_SUBTYPE_OMNI_REFERENCE, - aiPlatform: "GATEWAY", origin: "canvas", - model: "", aiModelId: 15, aiModelCode: "viduq2-pro", aiModelCodeAlias: "viduq2-pro", - batchSize: 1, ratio: "16:9", - aspect_ratio: "16:9", resolution: "720", videoRatio: 10, }); @@ -197,12 +197,48 @@ describe("popiserver generation provider", () => { }, referenceSubjectList: [ { id: 1, type: "image", url: "https://example.com/1.png", name: "图1" }, - { id: 2, type: "image", url: "https://example.com/2.png", name: "图2" }, ], }); expect(body.generate_audio).toBeUndefined(); }); + it("uses the frontend referenceSubjectList order without rebuilding it from images", async () => { + vi.mocked(global.fetch).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + status: "0000", + message: "ok", + data: { id: 1198 }, + }), + } as Response); + + const input = makeInput(); + input.images = [ + "https://popitest-public-1313913486.cos.ap-guangzhou.myqcloud.com/media/image/2026/0702/9535.jpeg", + "https://popitest-public-1313913486.cos.ap-guangzhou.myqcloud.com/media/image/2026/0701/9225.png", + ]; + input.referenceSubjectList = [ + { + id: 1, + type: "image", + url: "https://popitest-public-1313913486.cos.ap-guangzhou.myqcloud.com/media/image/2026/0701/9225.png", + name: "图1", + }, + { + id: 2, + type: "image", + url: "https://popitest-public-1313913486.cos.ap-guangzhou.myqcloud.com/media/image/2026/0702/9535.jpeg", + name: "图2", + }, + ]; + + await submitPopiTask(makeRequest(), "login-token", input); + + const [, init] = vi.mocked(global.fetch).mock.calls[0]; + const body = JSON.parse(String(init?.body)); + expect(body.referenceSubjectList).toEqual(input.referenceSubjectList); + }); + it("collects all task ids from batched task create responses", async () => { vi.mocked(global.fetch).mockResolvedValue({ ok: true, @@ -239,10 +275,7 @@ describe("popiserver generation provider", () => { const [, init] = vi.mocked(global.fetch).mock.calls[0]; const body = JSON.parse(String(init?.body)); expect(body).toMatchObject({ - ratio: "9:16", aspect_ratio: "9:16", - width: 720, - height: 1280, }); expect(body.aspectRatio).toBeUndefined(); }); @@ -283,7 +316,6 @@ describe("popiserver generation provider", () => { const body = JSON.parse(String(init?.body)); expect(body).toMatchObject({ chatPrompt: "比例:16:9\n清晰度:1K\n提示词:create a cinematic cat", - model: "", ratio: "16:9", resolution: "1K", }); @@ -325,15 +357,13 @@ describe("popiserver generation provider", () => { const body = JSON.parse(String(init?.body)); expect(body).toMatchObject({ chatPrompt: "比例:9:16\n清晰度:2K\n提示词:create a clean product render", - model: "", ratio: "9:16", - aspect_ratio: "9:16", resolution: "2K", }); expect(body.aspectRatio).toBeUndefined(); }); - it("builds referenceSubjectList for image tasks without @ in names", async () => { + it("uses frontend referenceSubjectList for image tasks", async () => { vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: () => Promise.resolve({ @@ -359,18 +389,18 @@ describe("popiserver generation provider", () => { }; input.prompt = "@图1 作为人物参考生成海报"; input.images = ["https://statictest.popi.art/media/ani/character/2026/0512/4540.jpg"]; + input.referenceSubjectList = [ + { + id: 1, + type: "image", + url: "https://statictest.popi.art/media/ani/character/2026/0512/4540.jpg", + name: "图1", + }, + ]; input.subType = 103; input.parameters = { ratio: "16:9", resolution: "1K", - referenceSubjectList: [ - { - id: 1, - type: "image", - url: "stale-image-url", - name: "@图1", - }, - ], }; const result = await submitPopiTask(makeRequest(), "login-token", input); @@ -410,15 +440,14 @@ describe("popiserver generation provider", () => { expect(JSON.parse(String(init?.body))).toEqual({ type: 3, chatPrompt: "我是一个小白兔,白白的小白兔", - model: "", origin: "canvas", - aiPlatform: "GATEWAY", aiModelName: "Speech 2.8 HD", aiModelId: 30, aiModelCode: "speech-2.8-hd", aiModelCodeAlias: "speech-2.8-hd", subType: 301, - batchSize: 1, + referenceSubjectList: undefined, + extraTaskParams: {}, }); }); @@ -585,7 +614,7 @@ describe("popiserver generation provider", () => { ]); }); - it("builds referenceSubjectList from final image and video URLs", async () => { + it("uses frontend referenceSubjectList with image, video, and audio references", async () => { vi.mocked(global.fetch).mockResolvedValue({ ok: true, json: () => Promise.resolve({ @@ -612,6 +641,27 @@ describe("popiserver generation provider", () => { }; input.prompt = "@图1的人物参考@视频2的人物的动作跳舞"; input.images = ["https://statictest.popi.art/media/ani/character/2026/0512/4540.jpg"]; + input.referenceSubjectList = [ + { + id: 1, + type: "image", + url: "https://statictest.popi.art/media/ani/character/2026/0512/4540.jpg", + name: "图1", + }, + { + id: 2, + type: "video", + url: "https://statictest.popi.art/media/aiGen/2026/0603/5470.mp4", + name: "视频2", + duration: 5.033, + }, + { + id: 3, + type: "audio", + url: "https://statictest.popi.art/media/audio/2026/0603/5470.mp3", + name: "音频3", + }, + ]; input.dynamicInputs = { videos: ["https://statictest.popi.art/media/aiGen/2026/0603/5470.mp4"], voices: ["https://statictest.popi.art/media/audio/2026/0603/5470.mp3"], @@ -624,27 +674,6 @@ describe("popiserver generation provider", () => { resolution: "480P", duration: 5, batchSize: 1, - referenceSubjectList: [ - { - id: 1, - type: "image", - url: "stale-image-url", - name: "@图1", - }, - { - id: 2, - type: "video", - url: "stale-video-url", - name: "@视频2", - duration: 5.033, - }, - { - id: 3, - type: "audio", - url: "stale-audio-url", - name: "@音频3", - }, - ], }; input.subType = POPI_VIDEO_SUBTYPE_OMNI_REFERENCE; @@ -666,33 +695,11 @@ describe("popiserver generation provider", () => { resolution: "480P", duration: 5, batchSize: 1, - width: 1280, - height: 720, chatPrompt: "@图1的人物参考@视频2的人物的动作跳舞", images: ["https://statictest.popi.art/media/ani/character/2026/0512/4540.jpg"], videos: ["https://statictest.popi.art/media/aiGen/2026/0603/5470.mp4"], voices: ["https://statictest.popi.art/media/audio/2026/0603/5470.mp3"], - referenceSubjectList: [ - { - id: 1, - type: "image", - url: "https://statictest.popi.art/media/ani/character/2026/0512/4540.jpg", - name: "图1", - }, - { - id: 2, - type: "video", - url: "https://statictest.popi.art/media/aiGen/2026/0603/5470.mp4", - name: "视频2", - duration: 5.033, - }, - { - id: 3, - type: "audio", - url: "https://statictest.popi.art/media/audio/2026/0603/5470.mp3", - name: "音频3", - }, - ], + referenceSubjectList: input.referenceSubjectList, }); }); @@ -714,43 +721,13 @@ describe("popiserver generation provider", () => { input.dynamicInputs = { videos: ["https://statictest.popi.art/media/aiGen/2026/0603/5470.mp4"], }; - input.parameters = { - ...input.parameters, - referenceSubjectList: [ - { - id: 1, - type: "image", - url: "stale-image-url", - name: "@图1", - }, - { - id: 2, - type: "video", - url: "stale-video-url", - name: "@视频2", - }, - ], - }; await submitPopiTask(makeRequest(), "login-token", input); const [, init] = vi.mocked(global.fetch).mock.calls[0]; const body = JSON.parse(String(init?.body)); expect(body.videos).toBeUndefined(); - expect(body.referenceSubjectList).toEqual([ - { - id: 1, - type: "image", - url: "https://example.com/1.png", - name: "图1", - }, - { - id: 2, - type: "image", - url: "https://example.com/2.png", - name: "图2", - }, - ]); + expect(body.referenceSubjectList).toEqual(input.referenceSubjectList); }); it("extracts completed image task output from task list", async () => { diff --git a/src/utils/popiserverTaskPayload.ts b/src/utils/popiserverTaskPayload.ts index bd6dce12..402f6df2 100644 --- a/src/utils/popiserverTaskPayload.ts +++ b/src/utils/popiserverTaskPayload.ts @@ -8,14 +8,6 @@ export type PopiModelDetailForTaskPayload = { aiModelCodeAlias?: string; }; -type ReferenceSubject = { - id: number; - type: "image" | "video" | "audio"; - url: string; - name: string; - duration?: number; -}; - const POPISERVER_LEGACY_AUDIO_KEYS = [ "audio", "audios", @@ -98,39 +90,6 @@ function buildPopiChatPrompt( ].join("\n"); } -function normalizeReferenceSubjectName(name: string): string { - return name.replace(/^@+/, ""); -} - -function referenceSubjectMetadata(referenceSubjectList: unknown): ReferenceSubject[] { - if (!Array.isArray(referenceSubjectList)) return []; - - return referenceSubjectList.flatMap((item) => { - if (!item || typeof item !== "object" || Array.isArray(item)) return []; - const subject = item as Record; - const type = - subject.type === "video" - ? "video" - : subject.type === "image" - ? "image" - : subject.type === "audio" - ? "audio" - : null; - if (!type) return []; - const id = asNumber(subject.id); - const name = asString(subject.name); - const url = asString(subject.url); - const duration = asNumber(subject.duration); - return [{ - id: id ?? 0, - type, - url: url ?? "", - name: normalizeReferenceSubjectName(name ?? ""), - ...(duration !== null ? { duration } : {}), - }]; - }); -} - function removeLegacyAudioParameters(parameters: Record): Record { return Object.fromEntries( Object.entries(parameters).filter( @@ -139,57 +98,6 @@ function removeLegacyAudioParameters(parameters: Record): Recor ); } -function buildReferenceSubjectList( - images: string[], - videos: string[], - voices: string[], - referenceSubjectList: unknown -): ReferenceSubject[] { - const metadata = referenceSubjectMetadata(referenceSubjectList); - const metadataByType = { - image: metadata.filter((item) => item.type === "image"), - video: metadata.filter((item) => item.type === "video"), - audio: metadata.filter((item) => item.type === "audio"), - }; - const subjects: ReferenceSubject[] = []; - - images.forEach((url, index) => { - const id = subjects.length + 1; - const meta = metadataByType.image[index]; - subjects.push({ - id, - type: "image", - url, - name: normalizeReferenceSubjectName(meta?.name || `图${id}`), - }); - }); - - videos.forEach((url, index) => { - const id = subjects.length + 1; - const meta = metadataByType.video[index]; - subjects.push({ - id, - type: "video", - url, - name: normalizeReferenceSubjectName(meta?.name || `视频${id}`), - ...(meta?.duration ? { duration: meta.duration } : {}), - }); - }); - - voices.forEach((url, index) => { - const id = subjects.length + 1; - const meta = metadataByType.audio[index]; - subjects.push({ - id, - type: "audio", - url, - name: normalizeReferenceSubjectName(meta?.name || `音频${id}`), - }); - }); - - return subjects; -} - export function buildPopiTaskBody( input: GenerationInput, modelDetail: PopiModelDetailForTaskPayload | null @@ -197,7 +105,7 @@ export function buildPopiTaskBody( const rawParameters = removeLegacyAudioParameters(input.parameters || {}); const rawExtraTaskParams = removeLegacyAudioParameters(input.extraTaskParams || {}); const aspectRatio = resolveAspectRatio(rawParameters); - const { referenceSubjectList: _referenceSubjectList, ...parameters } = rawParameters; + const parameters = rawParameters; const selectedModel = { provider: "popiserver" as const, modelId: input.model.id, @@ -226,9 +134,6 @@ export function buildPopiTaskBody( const voices = modelSupportsReferenceMedia(selectedModel, "audio", rawParameters) ? input.voices || [] : []; - const referenceSubjectList = type === 1 || type === 2 - ? buildReferenceSubjectList([...new Set(images)], [...new Set(videos)], [...new Set(voices)], input.referenceSubjectList) - : []; const voiceId = asString(input.audioVoiceId) ?? asString(parameters.voiceId) ?? @@ -249,7 +154,7 @@ export function buildPopiTaskBody( ...(asNumber(input.batchSize) !== null || asNumber(parameters.batchSize) !== null ? { batchSize: asNumber(input.batchSize) ?? asNumber(parameters.batchSize) } : {}), - referenceSubjectList, + referenceSubjectList: input.referenceSubjectList, extraTaskParams: rawExtraTaskParams, };