Browse Source

处理参数

feature/remove
TianYun 3 weeks ago
parent
commit
000b624229
  1. 33
      src/app/api/generate/providers/__tests__/popiserver.test.ts
  2. 3
      src/app/api/generate/providers/popiserver.ts
  3. 5
      src/app/api/generate/route.ts
  4. 16
      src/app/api/generate/task/create/__tests__/route.test.ts
  5. 3
      src/app/api/generate/task/create/route.ts
  6. 38
      src/app/api/points/calculate-task-price/__tests__/route.test.ts
  7. 15
      src/app/api/points/calculate-task-price/route.ts
  8. 2
      src/lib/popiartTask/runTask.ts
  9. 28
      src/utils/__tests__/referenceSubjectList.test.ts
  10. 17
      src/utils/referenceSubjectList.ts

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

@ -95,11 +95,40 @@ describe("popiserver generation provider", () => {
aiModelCodeAlias: "viduq2-pro",
batchSize: 1,
ratio: "16:9",
aspect_ratio: "16:9",
resolution: "720",
videoRatio: 10,
});
expect(body.aspectRatio).toBeUndefined();
expect(body.aspect_ratio).toBeUndefined();
});
it("maps aspect_ratio into PopiServer task ratio fields", async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve({
status: "0000",
message: "ok",
data: { id: 1195 },
}),
} as Response);
const input = makeInput();
input.parameters = {
aspect_ratio: "9:16",
resolution: "720",
};
await submitPopiTask(makeRequest(), "login-token", input);
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();
});
it("embeds ratio and resolution in chatPrompt for gpt-image-2-vip", async () => {
@ -182,10 +211,10 @@ describe("popiserver generation provider", () => {
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();
expect(body.aspect_ratio).toBeUndefined();
});
it("builds referenceSubjectList for image tasks without @ in names", async () => {

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

@ -332,7 +332,7 @@ function inferTaskSubType(input: GenerationInput, type: number): number {
}
function resolveAspectRatio(parameters: Record<string, unknown> | undefined): string {
return asString(parameters?.ratio) ?? asString(parameters?.aspectRatio) ?? "16:9";
return asString(parameters?.ratio) ?? asString(parameters?.aspectRatio) ?? asString(parameters?.aspect_ratio) ?? "16:9";
}
function removePopiRatioAliases(parameters: Record<string, unknown>): Record<string, unknown> {
@ -544,6 +544,7 @@ function buildPopiTaskBody(input: GenerationInput, modelDetail: PopiAIModel | nu
width: asNumber(parameters.width) ?? dimensions.width,
height: asNumber(parameters.height) ?? dimensions.height,
ratio: aspectRatio,
aspect_ratio: aspectRatio,
duration: asNumber(parameters.duration) ?? undefined,
resolution: asString(parameters.resolution) ?? "720p",
};

5
src/app/api/generate/route.ts

@ -25,6 +25,7 @@ import { ApiError, apiErrorResponse } from "../_errors";
import { resolveUserGatewayApiKey } from "../_gatewayApiKey";
import { resolveTemporaryImageUrlsInArray } from "@/lib/images/temporaryUrls";
import { isDataUrl, uploadDataUrlToPopiMedia } from "../_popiMedia";
import { normalizeReferenceSubjectListIds } from "@/utils/referenceSubjectList";
// Re-export for backward compatibility (test file imports from route)
export const clearFalInputMappingCache = _clearFalInputMappingCache;
@ -316,7 +317,7 @@ export async function POST(request: NextRequest) {
images: stringArrayFromUnknown(images),
videos: stringArrayFromUnknown(videos),
voices: stringArrayFromUnknown(voices),
parameters,
parameters: normalizeReferenceSubjectListIds(parameters),
dynamicInputs,
};
@ -357,7 +358,7 @@ export async function POST(request: NextRequest) {
images: stringArrayFromUnknown(images),
videos: stringArrayFromUnknown(videos),
voices: stringArrayFromUnknown(voices),
parameters,
parameters: normalizeReferenceSubjectListIds(parameters),
dynamicInputs,
};

16
src/app/api/generate/task/create/__tests__/route.test.ts

@ -42,7 +42,13 @@ describe("/api/generate/task/create route", () => {
isSupportAudios: true,
},
},
parameters: { resolution: "720p" },
parameters: {
resolution: "720p",
referenceSubjectList: [
{ id: "node-image", type: "image", url: "https://cdn.example.com/ref.png", name: "图1" },
{ id: "node-video", type: "video", url: "https://cdn.example.com/ref.mp4", name: "视频2", duration: 4 },
],
},
dynamicInputs: { prompt: "make a video" },
}));
@ -54,7 +60,13 @@ describe("/api/generate/task/create route", () => {
videos: ["https://cdn.example.com/ref.mp4"],
voices: ["https://cdn.example.com/ref.mp3"],
prompt: "make a video",
parameters: { resolution: "720p" },
parameters: {
resolution: "720p",
referenceSubjectList: [
{ id: 1, type: "image", url: "https://cdn.example.com/ref.png", name: "图1" },
{ id: 2, type: "video", url: "https://cdn.example.com/ref.mp4", name: "视频2", duration: 4 },
],
},
dynamicInputs: { prompt: "make a video" },
model: {
id: "15",

3
src/app/api/generate/task/create/route.ts

@ -4,6 +4,7 @@ import type { GenerationInput, ModelCapability } from "@/lib/providers/types";
import { submitPopiTask } from "../../providers/popiserver";
import { ApiError, apiErrorResponse } from "@/app/api/_errors";
import { requireLogin } from "@/app/api/_auth";
import { normalizeReferenceSubjectListIds } from "@/utils/referenceSubjectList";
export const dynamic = "force-dynamic";
export const maxDuration = 60;
@ -51,7 +52,7 @@ export async function POST(request: NextRequest) {
images: body.images ? [...body.images] : [],
videos: body.videos ? [...body.videos] : [],
voices: body.voices ? [...body.voices] : [],
parameters: body.parameters,
parameters: normalizeReferenceSubjectListIds(body.parameters),
dynamicInputs: body.dynamicInputs,
};

38
src/app/api/points/calculate-task-price/__tests__/route.test.ts

@ -83,4 +83,42 @@ describe("/api/points/calculate-task-price route", () => {
}
);
});
it("normalizes reference subject ids before forwarding task price requests", async () => {
process.env.POPIART_API_BASE_URL = "https://popi.example";
const payload = {
type: 1,
subType: 102,
aiModelId: 32,
referenceSubjectList: [
{ id: "subject-image-a", type: "image", url: "https://example.com/a.png", name: "image 1" },
{ id: "subject-video-b", type: "video", url: "https://example.com/b.mp4", name: "video 2" },
],
};
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ success: true, data: { taskPrice: 24 } }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
global.fetch = fetchMock;
const response = await POST(createPostRequest(payload, {
Authorization: "Bearer user-token",
}));
expect(response.status).toBe(200);
expect(fetchMock).toHaveBeenCalledWith(
"https://popi.example/api_client/anime/task/calculateTaskPrice",
expect.objectContaining({
body: JSON.stringify({
...payload,
referenceSubjectList: [
{ id: 1, type: "image", url: "https://example.com/a.png", name: "image 1" },
{ id: 2, type: "video", url: "https://example.com/b.mp4", name: "video 2" },
],
}),
})
);
});
});

15
src/app/api/points/calculate-task-price/route.ts

@ -3,6 +3,7 @@ import { requireLogin } from "@/app/api/_auth";
import { ApiError, apiErrorResponse } from "@/app/api/_errors";
import { logGatewayRequest, logGatewayResponse } from "@/app/api/_gatewayLogging";
import { getPopiartApiBaseUrl } from "@/app/api/_popiartBaseUrl";
import { normalizeReferenceSubjectListIds } from "@/utils/referenceSubjectList";
const UPSTREAM_CALCULATE_TASK_PRICE_PATH = "/api_client/anime/task/calculateTaskPrice";
@ -22,7 +23,9 @@ export async function POST(request: NextRequest) {
throw ApiError.badRequest("aiModelId is required");
}
console.info("[popiserver calculateTaskPrice] request body", body);
const upstreamBody = normalizeReferenceSubjectListIds(body) ?? body;
console.info("[popiserver calculateTaskPrice] request body", upstreamBody);
const url = getUpstreamUrl(request);
const init: RequestInit = {
@ -33,19 +36,19 @@ export async function POST(request: NextRequest) {
Authorization: `Bearer ${token}`,
token,
},
body: JSON.stringify(body),
body: JSON.stringify(upstreamBody),
cache: "no-store",
};
const startedAt = Date.now();
logGatewayRequest("Calling task price API", url, init, {
aiModelId: body.aiModelId,
subType: body.subType,
aiModelId: upstreamBody.aiModelId,
subType: upstreamBody.subType,
});
const response = await fetch(url, init);
await logGatewayResponse("Task price API response", url, init, response, {
aiModelId: body.aiModelId,
subType: body.subType,
aiModelId: upstreamBody.aiModelId,
subType: upstreamBody.subType,
}, startedAt);
const payload = await response.json().catch(() => null);
console.info("[popiserver calculateTaskPrice] response", {

2
src/lib/popiartTask/runTask.ts

@ -1,6 +1,7 @@
"use client";
import type { GenerateResponse, SelectedModel } from "@/types";
import { normalizeReferenceSubjectListIds } from "@/utils/referenceSubjectList";
export type PopiartTaskPayload = {
images: string[];
@ -43,6 +44,7 @@ function omitSelectedModelProvider(selectedModel: SelectedModel): Omit<SelectedM
function serializeTaskCreatePayload(payload: PopiartTaskPayload): string {
return JSON.stringify({
...payload,
parameters: normalizeReferenceSubjectListIds(payload.parameters),
selectedModel: omitSelectedModelProvider(payload.selectedModel),
});
}

28
src/utils/__tests__/referenceSubjectList.test.ts

@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { normalizeReferenceSubjectListIds } from "../referenceSubjectList";
describe("normalizeReferenceSubjectListIds", () => {
it("maps reference subject ids to numeric sequence values", () => {
const parameters = normalizeReferenceSubjectListIds({
referenceSubjectList: [
{ id: "node-a", type: "image", url: "https://example.com/a.png", name: "图1" },
{ id: "node-b", type: "video", url: "https://example.com/b.mp4", name: "视频2", duration: 3 },
],
resolution: "720p",
});
expect(parameters).toEqual({
referenceSubjectList: [
{ id: 1, type: "image", url: "https://example.com/a.png", name: "图1" },
{ id: 2, type: "video", url: "https://example.com/b.mp4", name: "视频2", duration: 3 },
],
resolution: "720p",
});
});
it("leaves parameters without reference subjects unchanged", () => {
const parameters = { resolution: "720p" };
expect(normalizeReferenceSubjectListIds(parameters)).toBe(parameters);
});
});

17
src/utils/referenceSubjectList.ts

@ -0,0 +1,17 @@
export function normalizeReferenceSubjectListIds(
parameters: Record<string, unknown> | undefined
): Record<string, unknown> | undefined {
const referenceSubjectList = parameters?.referenceSubjectList;
if (!Array.isArray(referenceSubjectList)) return parameters;
return {
...parameters,
referenceSubjectList: referenceSubjectList.map((item, index) => {
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
return {
...item,
id: index + 1,
};
}),
};
}
Loading…
Cancel
Save