weige 2 months ago
parent
commit
7fbe80e0a2
  1. 81
      src/app/api/generate/providers/__tests__/popiserver.test.ts
  2. 30
      src/app/api/generate/providers/popiserver.ts
  3. 128
      src/components/__tests__/GenerationComposer.test.tsx
  4. 13
      src/components/composer/GenerationComposer.tsx

81
src/app/api/generate/providers/__tests__/popiserver.test.ts

@ -93,11 +93,92 @@ describe("popiserver generation provider", () => {
aiModelCodeAlias: "viduq2-pro", aiModelCodeAlias: "viduq2-pro",
batchSize: 1, batchSize: 1,
ratio: "16:9", ratio: "16:9",
aspectRatio: "16:9",
resolution: "720", resolution: "720",
videoRatio: 10, 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 () => { it("submits audio task create with the gateway audio request shape", async () => {
vi.mocked(global.fetch).mockResolvedValue({ vi.mocked(global.fetch).mockResolvedValue({
ok: true, ok: true,

30
src/app/api/generate/providers/popiserver.ts

@ -387,7 +387,7 @@ function inferTaskSubType(input: GenerationInput, type: number): number {
} }
function resolveAspectRatio(parameters: Record<string, unknown> | undefined): string { function resolveAspectRatio(parameters: Record<string, unknown> | 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 } { function dimensionsForAspectRatio(aspectRatio: string): { width: number; height: number } {
@ -396,6 +396,32 @@ function dimensionsForAspectRatio(aspectRatio: string): { width: number; height:
return { width: 1280, height: 720 }; 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<string, unknown>,
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<string, unknown> { function buildPopiTaskBody(input: GenerationInput, modelDetail: PopiAIModel | null): Record<string, unknown> {
const parameters = input.parameters || {}; const parameters = input.parameters || {};
const type = asNumber(parameters.type) ?? inferTaskType(input); const type = asNumber(parameters.type) ?? inferTaskType(input);
@ -427,7 +453,7 @@ function buildPopiTaskBody(input: GenerationInput, modelDetail: PopiAIModel | nu
const commonBody = { const commonBody = {
type, type,
chatPrompt: input.prompt, chatPrompt: buildPopiChatPrompt(input.prompt, parameters, aspectRatio, modelCode, modelAlias),
model: modelAlias, model: modelAlias,
origin: "canvas", origin: "canvas",
aiPlatform: "GATEWAY", aiPlatform: "GATEWAY",

128
src/components/__tests__/GenerationComposer.test.tsx

@ -714,11 +714,63 @@ 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(<GenerationComposer />);
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("does not show PopiServer task price estimates for token-billed models", async () => { it("does not show PopiServer task price estimates for token-billed models", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
throw new Error(`Unexpected fetch: ${String(input)}`); throw new Error(`Unexpected fetch: ${String(input)}`);
}); });
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
useWorkflowStore.setState({ useWorkflowStore.setState({
nodes: [ nodes: [
imageNode("img-1", true, { imageNode("img-1", true, {
@ -918,6 +970,82 @@ describe("GenerationComposer", () => {
expect(data.soundEnabled).toBe(false); 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(<GenerationComposer />);
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", () => { it("shows custom selected video duration in quick settings", () => {
useWorkflowStore.setState({ useWorkflowStore.setState({
nodes: [videoNode("vid-1", true, { durationSeconds: 6 })], nodes: [videoNode("vid-1", true, { durationSeconds: 6 })],

13
src/components/composer/GenerationComposer.tsx

@ -207,13 +207,12 @@ function buildGatewayDimensionSelects(
.filter((param) => nameSet.has(param.name) && param.options && param.options.length > 0) .filter((param) => nameSet.has(param.name) && param.options && param.options.length > 0)
.map((parameter) => { .map((parameter) => {
const options = getSchemaSelectOptions(parameter); const options = getSchemaSelectOptions(parameter);
let value = draft.parameters?.[parameter.name]; const value = draft.parameters?.[parameter.name]
if (value === undefined) { ?? (parameter.name === "ratio" ? draft.aspectRatio : undefined)
if (parameter.name === "ratio") value = draft.aspectRatio; ?? (parameter.name === "resolution" ? draft.resolution : undefined)
if (parameter.name === "resolution") value = draft.resolution; ?? (parameter.name === "duration" ? draft.durationSeconds : undefined)
if (parameter.name === "duration") value = draft.durationSeconds; ?? parameter.default
} ?? options[0]?.value;
value ??= parameter.default ?? options[0]?.value;
return { return {
parameter, parameter,
options, options,

Loading…
Cancel
Save