Browse Source

尺寸处理

feature/071
yun 1 week ago
parent
commit
7cfb60b670
  1. 10
      src/app/api/generate/providers/__tests__/popiserver.test.ts
  2. 66
      src/app/api/generate/providers/popiserver.ts
  3. 2
      src/app/api/generate/route.ts
  4. 1
      src/app/api/generate/task/poll/route.ts
  5. 11
      src/components/nodes/GenerateImageNode.tsx
  6. 1
      src/lib/providers/types.ts
  7. 45
      src/store/execution/__tests__/nanoBananaExecutor.test.ts
  8. 2
      src/types/api.ts

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

@ -787,6 +787,7 @@ describe("popiserver generation provider", () => {
images: ["https://example.com/result.png"],
originImages: ["https://example.com/origin.png"],
thumbs: ["https://example.com/result_thumb.webp"],
dimensions: [{ width: 1600, height: 900 }],
},
],
},
@ -807,6 +808,7 @@ describe("popiserver generation provider", () => {
type: "image",
url: "https://example.com/origin.png",
previewImg: "https://example.com/result_thumb.webp",
dimensions: { width: 1600, height: 900 },
});
}
});
@ -823,8 +825,8 @@ describe("popiserver generation provider", () => {
id: 1191,
status: 2,
resultList: [
{ images: ["https://example.com/result-1.png"] },
{ images: ["https://example.com/result-2.png"] },
{ images: ["https://example.com/result-1.png"], width: 1200, height: 800 },
{ images: ["https://example.com/result-2.png"], widths: [900], heights: [1200] },
{ images: ["https://example.com/result-3.png"] },
],
},
@ -838,8 +840,8 @@ describe("popiserver generation provider", () => {
expect(result.status).toBe("completed");
if (result.status === "completed") {
expect(result.output.outputs).toEqual([
{ type: "image", data: "", url: "https://example.com/result-1.png" },
{ type: "image", data: "", url: "https://example.com/result-2.png" },
{ type: "image", data: "", url: "https://example.com/result-1.png", dimensions: { width: 1200, height: 800 } },
{ type: "image", data: "", url: "https://example.com/result-2.png", dimensions: { width: 900, height: 1200 } },
{ type: "image", data: "", url: "https://example.com/result-3.png" },
]);
}

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

@ -61,6 +61,15 @@ type PopiTaskResult = {
cover?: string | null;
coverThumb?: string | null;
originImages?: string[] | null;
width?: number | string | null;
height?: number | string | null;
widths?: Array<number | string | null> | null;
heights?: Array<number | string | null> | null;
dimensions?: unknown;
imageWidth?: number | string | null;
imageHeight?: number | string | null;
imageWidths?: Array<number | string | null> | null;
imageHeights?: Array<number | string | null> | null;
};
type PopiTask = {
@ -123,6 +132,48 @@ function collectStrings(values: unknown[]): string[] {
return items;
}
function readPositiveNumber(value: unknown): number | null {
const numberValue = typeof value === "number"
? value
: typeof value === "string" && value.trim().length > 0
? Number(value)
: Number.NaN;
return Number.isFinite(numberValue) && numberValue > 0 ? numberValue : null;
}
function readDimensionObject(value: unknown): { width: number; height: number } | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const record = value as Record<string, unknown>;
const width = readPositiveNumber(
record.width ?? record.w ?? record.imageWidth ?? record.originWidth ?? record.naturalWidth
);
const height = readPositiveNumber(
record.height ?? record.h ?? record.imageHeight ?? record.originHeight ?? record.naturalHeight
);
return width && height ? { width, height } : null;
}
function readIndexedDimension(value: unknown, index: number): { width: number; height: number } | null {
if (!value) return null;
if (Array.isArray(value)) {
return readDimensionObject(value[index]) ?? readIndexedDimension(value[0], index);
}
return readDimensionObject(value);
}
function readPopiImageDimensions(res: PopiTaskResult, index: number): { width: number; height: number } | null {
const dimensions = readIndexedDimension(res.dimensions, index);
if (dimensions) return dimensions;
const indexedWidth = readPositiveNumber(res.widths?.[index] ?? res.imageWidths?.[index]);
const indexedHeight = readPositiveNumber(res.heights?.[index] ?? res.imageHeights?.[index]);
if (indexedWidth && indexedHeight) return { width: indexedWidth, height: indexedHeight };
const width = readPositiveNumber(res.width ?? res.imageWidth);
const height = readPositiveNumber(res.height ?? res.imageHeight);
return width && height ? { width, height } : null;
}
function referenceSubjectUrls(referenceSubjectList: unknown, type: ReferenceSubjectMediaType): string[] {
if (!Array.isArray(referenceSubjectList)) return [];
return referenceSubjectList.flatMap((item) => {
@ -472,7 +523,7 @@ function extractOutputFromTask(task: PopiTask, mediaType: string): GenerationOut
? { success: true, outputs: [{ type: "audio", data: "", url }] }
: { success: false, error: "PopiServer audio task completed without output" };
}
const images: { image: string; previewImg?: string }[] = [];
const images: { image: string; previewImg?: string; dimensions?: { width: number; height: number } | null }[] = [];
task.resultList?.forEach((res) => {
const originImages = collectStrings(res?.originImages ?? []);
const resultImages = collectStrings(res?.images ?? []);
@ -484,7 +535,7 @@ function extractOutputFromTask(task: PopiTask, mediaType: string): GenerationOut
const previewImg =
thumbs[index] ||
(originImages.length > 0 && resultImage && resultImage !== image ? resultImage : undefined);
images.push({ image, previewImg });
images.push({ image, previewImg, dimensions: readPopiImageDimensions(res, index) });
});
});
// task.images is the original input/reference image list in Popiserver task
@ -492,7 +543,16 @@ function extractOutputFromTask(task: PopiTask, mediaType: string): GenerationOut
// not put generated images under resultList.
return images.length > 0
? { success: true, outputs: images.map((img) => ({ type: "image", data: "", url: img.image, previewImg: img.previewImg })) }
? {
success: true,
outputs: images.map((img) => ({
type: "image",
data: "",
url: img.image,
...(img.previewImg ? { previewImg: img.previewImg } : {}),
...(img.dimensions ? { dimensions: img.dimensions } : {}),
})),
}
: { success: false, error: "PopiServer image task completed without output" };
}

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

@ -108,6 +108,7 @@ export async function buildMediaResponse(
.map((item) => ({
url: item.data || item.url || "",
previewImg: item.previewImg,
dimensions: item.dimensions ?? null,
}))
.filter((item) => item.url.length > 0);
@ -116,6 +117,7 @@ export async function buildMediaResponse(
image: {
url: image,
previewImg: output.previewImg,
dimensions: output.dimensions ?? null,
},
...(images && images.length > 1 ? { images } : {}),
contentType: "image",

1
src/app/api/generate/task/poll/route.ts

@ -21,6 +21,7 @@ async function buildResultResponse(
.map((item) => ({
url: item.data || item.url || "",
previewImg: item.previewImg,
dimensions: item.dimensions ?? null,
}))
.filter((item) => item.url.length > 0);

11
src/components/nodes/GenerateImageNode.tsx

@ -598,7 +598,16 @@ export function GenerateImageNodeView({ id, data, selected }: GenerateImageNodeV
const dimensions = { width: image.naturalWidth, height: image.naturalHeight };
setCropImageDimensions(dimensions);
const currentDimensions = asImageDimensions(nodeData.dimensions);
if (currentDimensions?.width !== dimensions.width || currentDimensions?.height !== dimensions.height) {
const loadedOriginalImage = renderedImageSrc === nodeData.outputImage || displayImage === nodeData.outputImage;
const shouldPersistLoadedDimensions =
loadedOriginalImage ||
!currentDimensions ||
!nodeData.outputImage ||
nodeData.outputImage === nodeData.previewImg;
if (
shouldPersistLoadedDimensions &&
(currentDimensions?.width !== dimensions.width || currentDimensions?.height !== dimensions.height)
) {
updateMediaNodeData(id, { dimensions }, dimensions);
}
}

1
src/lib/providers/types.ts

@ -141,6 +141,7 @@ export interface GenerationOutputItem {
videoCover?: string;
previewImg?: string; // 为图片时封面图片
dimensions?: { width: number; height: number } | null;
}
export interface GenerationOutput {
/** Whether the generation succeeded */

45
src/store/execution/__tests__/nanoBananaExecutor.test.ts

@ -451,6 +451,51 @@ describe("executeNanoBanana", () => {
});
});
it("passes generated image dimensions to media node sizing", async () => {
const node = makeNode({
selectedModel: {
provider: "popiserver",
modelId: "32",
displayName: "Popi Image",
capabilities: ["text-to-image", "image-to-image"],
},
});
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true, taskId: "task-1" }),
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
status: "completed",
result: {
success: true,
image: {
url: "https://cdn.example.com/generated.png",
previewImg: "https://cdn.example.com/generated-thumb.webp",
dimensions: { width: 1600, height: 900 },
},
},
}),
});
const updateMediaNodeData = vi.fn();
const ctx = makeCtx(node, { updateMediaNodeData });
await executeNanoBanana(ctx);
expect(updateMediaNodeData).toHaveBeenCalledWith(
"gen-1",
expect.objectContaining({
outputImage: "https://cdn.example.com/generated.png",
previewImg: "https://cdn.example.com/generated-thumb.webp",
dimensions: { width: 1600, height: 900 },
status: "complete",
}),
{ width: 1600, height: 900 }
);
});
it("uses the generated image as multi-angle previewImage when no preview is returned", async () => {
const node = makeNode({
selectedModel: {

2
src/types/api.ts

@ -28,10 +28,12 @@ export interface GenerateResponse {
image?: {
url: string; // For large images, return URL directly
previewImg?: string; // Base64 data URL for smaller images
dimensions?: { width: number; height: number } | null;
};
images?: {
url: string; // For large images, return URL directly
previewImg?: string; // Base64 data URL for smaller images
dimensions?: { width: number; height: number } | null;
}[];
video?: string;
videoUrl?: string; // For large videos, return URL directly

Loading…
Cancel
Save