From d3ef42ee3263026af7f883a38d45f6a700412639 Mon Sep 17 00:00:00 2001 From: TianYun Date: Thu, 28 May 2026 15:45:41 +0800 Subject: [PATCH] =?UTF-8?q?=E7=89=B9=E6=AE=8A=E6=A8=A1=E5=9E=8B=E5=8D=95?= =?UTF-8?q?=E7=8B=AC=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 | 81 +++++++++++ src/app/api/generate/providers/popiserver.ts | 30 ++++- .../__tests__/GenerationComposer.test.tsx | 127 ++++++++++++++++++ .../composer/GenerationComposer.tsx | 11 +- 4 files changed, 244 insertions(+), 5 deletions(-) diff --git a/src/app/api/generate/providers/__tests__/popiserver.test.ts b/src/app/api/generate/providers/__tests__/popiserver.test.ts index 23359b82..d441a7af 100644 --- a/src/app/api/generate/providers/__tests__/popiserver.test.ts +++ b/src/app/api/generate/providers/__tests__/popiserver.test.ts @@ -93,11 +93,92 @@ describe("popiserver generation provider", () => { aiModelCodeAlias: "viduq2-pro", batchSize: 1, ratio: "16:9", + aspectRatio: "16:9", resolution: "720", videoRatio: 10, }); }); + it("embeds ratio and resolution in chatPrompt for gpt-image-2-vip", async () => { + vi.mocked(global.fetch).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + status: "0000", + message: "ok", + data: { id: 1188 }, + }), + } as Response); + + const input = makeInput(); + input.model = { + ...input.model, + id: "gpt-image-2-vip", + name: "GPT Image 2 VIP", + capabilities: ["text-to-image"], + metadata: { + type: 1, + subType: 102, + aiModelId: 88, + aiModelCode: "gpt-image-2-vip", + aiModelCodeAlias: "gpt-image-2-vip", + }, + }; + input.prompt = "create a cinematic cat"; + input.parameters = { + ratio: "16:9", + resolution: "1K", + }; + + await submitPopiTask(makeRequest(), "login-token", input); + + const [, init] = vi.mocked(global.fetch).mock.calls[0]; + expect(JSON.parse(String(init?.body))).toMatchObject({ + chatPrompt: "比例:16:9\n清晰度:1K\n提示词:create a cinematic cat", + ratio: "16:9", + resolution: "1K", + }); + }); + + it("embeds ratio and resolution in chatPrompt for gpt-image-2-all", async () => { + vi.mocked(global.fetch).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + status: "0000", + message: "ok", + data: { id: 1187 }, + }), + } as Response); + + const input = makeInput(); + input.model = { + ...input.model, + id: "gpt-image-2-all", + name: "GPT Image 2 All", + capabilities: ["text-to-image"], + metadata: { + type: 1, + subType: 102, + aiModelId: 87, + aiModelCode: "gpt-image-2-all", + aiModelCodeAlias: "gpt-image-2-all", + }, + }; + input.prompt = "create a clean product render"; + input.parameters = { + aspectRatio: "9:16", + resolution: "2K", + }; + + await submitPopiTask(makeRequest(), "login-token", input); + + const [, init] = vi.mocked(global.fetch).mock.calls[0]; + expect(JSON.parse(String(init?.body))).toMatchObject({ + chatPrompt: "比例:9:16\n清晰度:2K\n提示词:create a clean product render", + aspectRatio: "9:16", + resolution: "2K", + }); + }); + it("submits audio task create with the gateway audio request shape", async () => { vi.mocked(global.fetch).mockResolvedValue({ ok: true, diff --git a/src/app/api/generate/providers/popiserver.ts b/src/app/api/generate/providers/popiserver.ts index 763524b1..3a3093bc 100644 --- a/src/app/api/generate/providers/popiserver.ts +++ b/src/app/api/generate/providers/popiserver.ts @@ -387,7 +387,7 @@ function inferTaskSubType(input: GenerationInput, type: number): number { } function resolveAspectRatio(parameters: Record | undefined): string { - return asString(parameters?.ratio) ?? "16:9"; + return asString(parameters?.ratio) ?? asString(parameters?.aspectRatio) ?? "16:9"; } function dimensionsForAspectRatio(aspectRatio: string): { width: number; height: number } { @@ -396,6 +396,32 @@ function dimensionsForAspectRatio(aspectRatio: string): { width: number; height: return { width: 1280, height: 720 }; } +function needsPromptEmbeddedParameters(modelCode: string, modelAlias: string): boolean { + return ( + modelCode === "gpt-image-2-vip" || + modelCode === "gpt-image-2-all" || + modelAlias === "gpt-image-2-vip" || + modelAlias === "gpt-image-2-all" + ); +} + +function buildPopiChatPrompt( + prompt: string, + parameters: Record, + aspectRatio: string, + modelCode: string, + modelAlias: string +): string { + if (!needsPromptEmbeddedParameters(modelCode, modelAlias)) return prompt; + + const resolution = asString(parameters.resolution) ?? "1K"; + return [ + `比例:${aspectRatio}`, + `清晰度:${resolution}`, + `提示词:${prompt}`, + ].join("\n"); +} + function buildPopiTaskBody(input: GenerationInput, modelDetail: PopiAIModel | null): Record { const parameters = input.parameters || {}; const type = asNumber(parameters.type) ?? inferTaskType(input); @@ -427,7 +453,7 @@ function buildPopiTaskBody(input: GenerationInput, modelDetail: PopiAIModel | nu const commonBody = { type, - chatPrompt: input.prompt, + chatPrompt: buildPopiChatPrompt(input.prompt, parameters, aspectRatio, modelCode, modelAlias), model: modelAlias, origin: "canvas", aiPlatform: "GATEWAY", diff --git a/src/components/__tests__/GenerationComposer.test.tsx b/src/components/__tests__/GenerationComposer.test.tsx index 9d335a86..3c52cefd 100644 --- a/src/components/__tests__/GenerationComposer.test.tsx +++ b/src/components/__tests__/GenerationComposer.test.tsx @@ -713,6 +713,57 @@ describe("GenerationComposer", () => { }); }); + it("renders gateway image dimension options by schema name", () => { + useWorkflowStore.setState({ + nodes: [ + imageNode("img-1", true, { + selectedModel: { + provider: "popiserver", + modelId: "37", + displayName: "Gateway Image", + capabilities: ["text-to-image", "image-to-image"], + }, + parameterSchema: [ + { + name: "ratio", + type: "string", + label: "Ratio", + options: [ + { label: "16:9", value: "16:9" }, + { label: "9:16", value: "9:16" }, + ], + default: "16:9", + }, + { + name: "resolution", + type: "string", + label: "Resolution", + options: [ + { label: "720p", value: "720" }, + { label: "4k", value: "4k" }, + ], + default: "720", + }, + ], + parameters: {}, + }), + ], + }); + + render(); + + act(() => { + fireEvent.change(screen.getByLabelText("Ratio"), { target: { value: "9:16" } }); + fireEvent.change(screen.getByLabelText("Resolution"), { target: { value: "4k" } }); + }); + + const data = useWorkflowStore.getState().nodes[0].data as NanoBananaNodeData; + expect(data.parameters).toMatchObject({ + ratio: "9:16", + resolution: "4k", + }); + }); + it("uses the Seedance estimate endpoint for PopiServer Seedance models", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = String(input); @@ -883,6 +934,82 @@ describe("GenerationComposer", () => { expect(data.soundEnabled).toBe(false); }); + it("renders gateway video dimension options with raw values", () => { + useWorkflowStore.setState({ + nodes: [ + videoNode("vid-1", true, { + selectedModel: { + provider: "popiserver", + modelId: "38", + displayName: "Gateway Video", + capabilities: ["text-to-video", "image-to-video"], + }, + parameterSchema: [ + { + name: "ratio", + type: "string", + label: "Ratio", + options: [ + { label: "16:9", value: "16:9" }, + { label: "9:16", value: "9:16" }, + ], + default: "16:9", + }, + { + name: "videoRatio", + type: "integer", + label: "Video ratio", + options: [ + { label: "10-10", value: 10 }, + { label: "20-20", value: 20 }, + ], + default: 10, + }, + { + name: "resolution", + type: "string", + label: "Resolution", + options: [ + { label: "720p", value: "720" }, + { label: "1024p", value: "1024" }, + ], + default: "720", + }, + { + name: "duration", + type: "integer", + label: "Duration", + options: [ + { label: "5", value: 5 }, + { label: "4", value: 4 }, + ], + default: 5, + }, + ], + parameters: {}, + }), + ], + }); + + render(); + + act(() => { + fireEvent.change(screen.getByLabelText("Ratio"), { target: { value: "9:16" } }); + fireEvent.change(screen.getByLabelText("Video ratio"), { target: { value: "20" } }); + fireEvent.change(screen.getByLabelText("Resolution"), { target: { value: "1024" } }); + fireEvent.change(screen.getByLabelText("Duration"), { target: { value: "4" } }); + }); + + const data = useWorkflowStore.getState().nodes[0].data as GenerateVideoNodeData; + expect(data.parameters).toEqual({ + ratio: "9:16", + videoRatio: 20, + resolution: "1024", + duration: 4, + }); + expect(data.durationSeconds).toBe(4); + }); + it("shows custom selected video duration in quick settings", () => { useWorkflowStore.setState({ nodes: [videoNode("vid-1", true, { durationSeconds: 6 })], diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index 70cc1f1a..bbbf2087 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -187,7 +187,7 @@ function getSelectedSchemaOptionKey(options: SchemaSelectOption[], value: unknow function buildGatewayDimensionSelects( schema: ModelParameter[] | undefined, - parameters: Record | undefined + draft: ComposerDraft ): GatewayDimensionSelect[] { if (!schema || schema.length === 0) return []; const nameSet = new Set(GATEWAY_DIMENSION_PARAMETER_NAMES); @@ -195,7 +195,12 @@ function buildGatewayDimensionSelects( .filter((param) => nameSet.has(param.name) && param.options && param.options.length > 0) .map((parameter) => { const options = getSchemaSelectOptions(parameter); - const value = parameters?.[parameter.name] ?? parameter.default ?? options[0]?.value; + const value = draft.parameters?.[parameter.name] + ?? (parameter.name === "ratio" ? draft.aspectRatio : undefined) + ?? (parameter.name === "resolution" ? draft.resolution : undefined) + ?? (parameter.name === "duration" ? draft.durationSeconds : undefined) + ?? parameter.default + ?? options[0]?.value; return { parameter, options, @@ -1398,7 +1403,7 @@ export function GenerationComposer() { ? context.node?.data as NanoBananaNodeData | GenerateVideoNodeData | GenerateAudioNodeData | undefined : undefined; const activeParameterSchema = generationNodeData?.parameterSchema ?? []; - const gatewayDimensionSelects = buildGatewayDimensionSelects(activeParameterSchema, draft.parameters); + const gatewayDimensionSelects = buildGatewayDimensionSelects(activeParameterSchema, draft); const imageCountOptions = getImageCountOptions(activeParameterSchema); const visibleImageCountOptions = imageCountOptions.length > 0 ? imageCountOptions