Normalize image output dimensions

This commit is contained in:
134 2026-06-13 13:33:02 +08:00
parent a265a77095
commit 4eac5cf0c0
10 changed files with 261 additions and 183 deletions

View File

@ -34,6 +34,7 @@ import {
import { acquireCanvasImageModelSlot } from "@/lib/canvas/image-concurrency";
import { createLogger } from "@/lib/observability/logger";
import { resolveModelRuntimeId } from "@/lib/ai/model-runtime";
import { resolveLongestEdgeDimensions } from "@/lib/ai/image-size-constraints";
// 请求体 Schema
const sceneFusionRequestSchema = z.object({
@ -1264,32 +1265,21 @@ function calculateSize(aspectRatio: string, imageSize: string): string | undefin
"4K": 2048,
};
const baseSize = baseSizeMap[imageSize] || 1536;
// 根据比例计算宽高
const ratioMap: Record<string, [number, number]> = {
"1:1": [1, 1],
"4:3": [4, 3],
"3:4": [3, 4],
"16:9": [16, 9],
"9:16": [9, 16],
};
const ratio = ratioMap[aspectRatio];
if (!ratio) {
if (!/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/.test(aspectRatio)) {
return undefined;
}
const [w, h] = ratio;
// 保持总像素接近 baseSize * baseSize,同时维持比例
const scale = Math.sqrt((baseSize * baseSize) / (w * h));
let width = Math.round(w * scale);
let height = Math.round(h * scale);
// 确保宽高在 512-2048 范围内
const clamp = (v: number) => Math.max(512, Math.min(2048, v));
width = clamp(width);
height = clamp(height);
const { width, height } = resolveLongestEdgeDimensions({
aspectRatio,
longestEdge: baseSize,
constraints: {
maxEdge: 2048,
maxPixels: 2048 * 2048,
minEdge: 512,
multiple: 2,
},
});
return `${width}*${height}`;
}

View File

@ -0,0 +1,96 @@
export type ImageSizeBucket = "1K" | "2K" | "4K";
export type ImageDimensionConstraints = {
maxEdge?: number;
maxPixels?: number;
minEdge?: number;
multiple?: number;
};
export type ImageDimensions = {
width: number;
height: number;
};
export function normalizeImageSizeBucket(value?: string, fallback: ImageSizeBucket = "2K"): ImageSizeBucket {
const normalized = String(value || "").trim().toUpperCase();
if (normalized === "1K" || normalized === "2K" || normalized === "4K") return normalized;
return fallback;
}
export function parseAspectRatioPair(value?: string): ImageDimensions | null {
const normalized = String(value || "").trim().toLowerCase();
if (!normalized || normalized === "auto") return null;
const match = normalized.match(/^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/);
if (!match) return null;
const width = Number(match[1]);
const height = Number(match[2]);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return null;
}
return { width, height };
}
export function roundToMultiple(value: number, multiple = 1) {
return Math.max(multiple, Math.round(value / multiple) * multiple);
}
export function floorToMultiple(value: number, multiple = 1) {
return Math.max(multiple, Math.floor(value / multiple) * multiple);
}
export function constrainImageDimensions(
dimensions: ImageDimensions,
constraints: ImageDimensionConstraints = {},
): ImageDimensions {
const multiple = Math.max(1, Math.floor(constraints.multiple || 1));
const minEdge = Math.max(multiple, Math.floor(constraints.minEdge || multiple));
const maxEdge = Number.isFinite(constraints.maxEdge)
? Math.max(minEdge, Math.floor(Number(constraints.maxEdge)))
: Number.POSITIVE_INFINITY;
const maxPixels = Number.isFinite(constraints.maxPixels)
? Math.max(minEdge * minEdge, Math.floor(Number(constraints.maxPixels)))
: Number.POSITIVE_INFINITY;
let width = Math.max(minEdge, floorToMultiple(Math.max(1, dimensions.width), multiple));
let height = Math.max(minEdge, floorToMultiple(Math.max(1, dimensions.height), multiple));
while (width > maxEdge || height > maxEdge || width * height > maxPixels) {
const scale = Math.min(maxEdge / width, maxEdge / height, Math.sqrt(maxPixels / (width * height)), 1);
const nextWidth = Math.max(minEdge, floorToMultiple(width * scale, multiple));
const nextHeight = Math.max(minEdge, floorToMultiple(height * scale, multiple));
if (nextWidth === width && nextHeight === height) {
if (width >= height && width > minEdge) width -= multiple;
else if (height > minEdge) height -= multiple;
else break;
} else {
width = nextWidth;
height = nextHeight;
}
}
return { width, height };
}
export function resolveLongestEdgeDimensions(params: {
aspectRatio?: string;
longestEdge: number;
multiple?: number;
constraints?: ImageDimensionConstraints;
}): ImageDimensions {
const ratioPair = parseAspectRatioPair(params.aspectRatio) || { width: 1, height: 1 };
const scale = params.longestEdge / Math.max(ratioPair.width, ratioPair.height);
return constrainImageDimensions(
{
width: ratioPair.width * scale,
height: ratioPair.height * scale,
},
{
multiple: params.multiple || params.constraints?.multiple,
...params.constraints,
},
);
}

View File

@ -9,6 +9,7 @@
*/
import sharp from "sharp";
import { constrainImageDimensions } from "@/lib/ai/image-size-constraints";
import { uploadFile } from "@/lib/storage/service";
interface AlibabaImageConfig {
@ -50,6 +51,7 @@ interface AlibabaImageResponse {
const ALIBABA_IMAGE_FETCH_TIMEOUT_MS = 20000;
const ALIBABA_MIN_SIZE = 512;
const ALIBABA_MAX_SIZE = 2048;
const ALIBABA_OUTPUT_SIZE_MULTIPLE = 2;
const ALIBABA_MAX_INPUT_SIDE = 4096;
const ALIBABA_DEFAULT_MAX_INPUT_BYTES = 18 * 1024 * 1024;
const ALIBABA_QWEN_MAX_INPUT_BYTES = 9.5 * 1024 * 1024;
@ -129,32 +131,7 @@ function calculateAlibabaSizeByAspect(aspectValue: number, baseSize: number): st
let height = Math.round(Math.sqrt(totalPixels / safeAspect));
let width = Math.round(height * safeAspect);
if (width > ALIBABA_MAX_SIZE) {
const scale = ALIBABA_MAX_SIZE / width;
width = ALIBABA_MAX_SIZE;
height = Math.round(height * scale);
}
if (height > ALIBABA_MAX_SIZE) {
const scale = ALIBABA_MAX_SIZE / height;
height = ALIBABA_MAX_SIZE;
width = Math.round(width * scale);
}
if (width < ALIBABA_MIN_SIZE) {
const scale = ALIBABA_MIN_SIZE / width;
width = ALIBABA_MIN_SIZE;
height = Math.round(height * scale);
}
if (height < ALIBABA_MIN_SIZE) {
const scale = ALIBABA_MIN_SIZE / height;
height = ALIBABA_MIN_SIZE;
width = Math.round(width * scale);
}
width = Math.max(ALIBABA_MIN_SIZE, Math.min(ALIBABA_MAX_SIZE, width));
height = Math.max(ALIBABA_MIN_SIZE, Math.min(ALIBABA_MAX_SIZE, height));
width = Math.round(width / 2) * 2;
height = Math.round(height / 2) * 2;
({ width, height } = constrainAlibabaOutputDimensions({ width, height }));
return `${width}*${height}`;
}
@ -211,33 +188,23 @@ function calculateAlibabaSizeFromDimensions(
let scaledWidth = Math.round(safeWidth * scale);
let scaledHeight = Math.round(safeHeight * scale);
if (scaledWidth > ALIBABA_MAX_SIZE || scaledHeight > ALIBABA_MAX_SIZE) {
const clampScale = Math.min(
ALIBABA_MAX_SIZE / scaledWidth,
ALIBABA_MAX_SIZE / scaledHeight,
);
scaledWidth = Math.round(scaledWidth * clampScale);
scaledHeight = Math.round(scaledHeight * clampScale);
}
if (scaledWidth < ALIBABA_MIN_SIZE || scaledHeight < ALIBABA_MIN_SIZE) {
const boostScale = Math.max(
ALIBABA_MIN_SIZE / scaledWidth,
ALIBABA_MIN_SIZE / scaledHeight,
);
scaledWidth = Math.round(scaledWidth * boostScale);
scaledHeight = Math.round(scaledHeight * boostScale);
}
scaledWidth = Math.max(ALIBABA_MIN_SIZE, Math.min(ALIBABA_MAX_SIZE, scaledWidth));
scaledHeight = Math.max(ALIBABA_MIN_SIZE, Math.min(ALIBABA_MAX_SIZE, scaledHeight));
scaledWidth = Math.round(scaledWidth / 2) * 2;
scaledHeight = Math.round(scaledHeight / 2) * 2;
({ width: scaledWidth, height: scaledHeight } = constrainAlibabaOutputDimensions({
width: scaledWidth,
height: scaledHeight,
}));
return `${scaledWidth}*${scaledHeight}`;
}
function constrainAlibabaOutputDimensions(dimensions: { width: number; height: number }) {
return constrainImageDimensions(dimensions, {
maxEdge: ALIBABA_MAX_SIZE,
maxPixels: ALIBABA_MAX_SIZE * ALIBABA_MAX_SIZE,
minEdge: ALIBABA_MIN_SIZE,
multiple: ALIBABA_OUTPUT_SIZE_MULTIPLE,
});
}
/**
* 创建缩略图
*/

View File

@ -1,6 +1,7 @@
import "server-only";
import sharp from "sharp";
import { resolveLongestEdgeDimensions } from "@/lib/ai/image-size-constraints";
import { uploadFile } from "@/lib/storage/service";
type BflImageConfig = {
@ -160,30 +161,22 @@ function parseAspectRatioValue(ratio: string) {
return width / height;
}
function roundToMultipleOf32(value: number) {
return Math.max(32, Math.round(value / 32) * 32);
}
function resolveBflDimensions(aspectRatio: string, imageSize: "1K" | "2K" | "4K") {
const baseMap: Record<typeof imageSize, number> = {
"1K": 1024,
"2K": 1536,
"4K": 2048,
};
const base = baseMap[imageSize] || 1536;
const ratioValue = parseAspectRatioValue(aspectRatio);
if (ratioValue >= 1) {
return {
width: base,
height: roundToMultipleOf32(base / ratioValue),
};
}
return {
width: roundToMultipleOf32(base * ratioValue),
height: base,
};
return resolveLongestEdgeDimensions({
aspectRatio,
longestEdge: baseMap[imageSize] || 1536,
constraints: {
maxEdge: 4096,
maxPixels: 4096 * 4096,
minEdge: 32,
multiple: 32,
},
});
}
async function inferAspectRatioFromImageSource(imageUrl: string): Promise<string | null> {

View File

@ -2,6 +2,10 @@ import "server-only";
import { createHash } from "node:crypto";
import sharp from "sharp";
import {
constrainImageDimensions,
resolveLongestEdgeDimensions,
} from "@/lib/ai/image-size-constraints";
import { uploadFile } from "@/lib/storage/service";
interface FalImageConfig {
@ -43,6 +47,10 @@ const DEFAULT_RESOLUTION = "1K";
const DEFAULT_OUTPUT_FORMAT = "png";
const DEFAULT_SAFETY_TOLERANCE = "4";
const DEFAULT_TIMEOUT_MS = 300000;
const FAL_GENERIC_MAX_EDGE = 4096;
const FAL_GENERIC_MAX_PIXELS = 4096 * 4096;
const GPT_IMAGE_2_MAX_EDGE = 3840;
const GPT_IMAGE_2_MAX_PIXELS = 8_294_400;
const FAL_NANO_BANANA_MODEL_IDS = new Set([
"fal-ai/nano-banana",
"fal-ai/nano-banana-2",
@ -234,35 +242,39 @@ function normalizeAnyExplicitAspectRatio(aspectRatio?: string) {
return normalized === "auto" ? undefined : normalized;
}
function parseAspectRatioValue(aspectRatio?: string) {
const normalized = normalizeAnyExplicitAspectRatio(aspectRatio);
if (!normalized) return undefined;
const [width, height] = normalized.split(":").map((part) => Number(part));
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return undefined;
}
return width / height;
}
function roundToMultiple(value: number, multiple: number) {
return Math.max(multiple, Math.round(value / multiple) * multiple);
}
function resolveFalCustomImageSize(aspectRatio?: string, resolution?: string): FalImageSize {
const ratio = parseAspectRatioValue(aspectRatio) || 1;
const bucket = normalizeResolution(resolution);
const areaByBucket: Record<string, number> = {
"0.5K": 768 * 768,
"1K": 1024 * 1024,
"2K": 1536 * 1536,
"4K": 2048 * 2048,
const longestEdgeByBucket: Record<string, number> = {
"0.5K": 768,
"1K": 1024,
"2K": 1536,
"4K": 2048,
};
const targetArea = areaByBucket[bucket] || areaByBucket["1K"];
const width = roundToMultiple(Math.sqrt(targetArea * ratio), 16);
const height = roundToMultiple(width / ratio, 16);
const { width, height } = resolveLongestEdgeDimensions({
aspectRatio,
longestEdge: longestEdgeByBucket[bucket] || longestEdgeByBucket["1K"],
constraints: {
maxEdge: FAL_GENERIC_MAX_EDGE,
maxPixels: FAL_GENERIC_MAX_PIXELS,
minEdge: 256,
multiple: 16,
},
});
return { width, height };
}
function constrainFalImageSize(size: FalImageSize, constraints = {
maxEdge: FAL_GENERIC_MAX_EDGE,
maxPixels: FAL_GENERIC_MAX_PIXELS,
}) {
if (typeof size === "string") return size;
return constrainImageDimensions(size, {
...constraints,
minEdge: 256,
multiple: 16,
});
}
function normalizeFalImageSize(imageSize?: string, aspectRatio?: string): FalImageSize {
const rawSize = String(imageSize || "").trim();
const normalizedSize = rawSize.toLowerCase().replace(/\s+/g, "");
@ -282,10 +294,7 @@ function normalizeFalImageSize(imageSize?: string, aspectRatio?: string): FalIma
width <= 4096 &&
height <= 4096
) {
return {
width: roundToMultiple(width, 16),
height: roundToMultiple(height, 16),
};
return constrainFalImageSize({ width, height });
}
}
@ -393,18 +402,24 @@ function normalizeGptImage2EditImageSize(
width % 16 === 0 &&
height % 16 === 0
) {
return { width, height };
return constrainFalImageSize(
{ width, height },
{ maxEdge: GPT_IMAGE_2_MAX_EDGE, maxPixels: GPT_IMAGE_2_MAX_PIXELS }
);
}
}
const sizeBucket = rawSize.toUpperCase();
const ratio = normalizeAspectRatio(aspectRatio).toLowerCase();
return (
const resolvedSize =
GPT_IMAGE_2_EDIT_SIZE_BY_BUCKET[sizeBucket]?.[ratio] ||
(normalizeAnyExplicitAspectRatio(aspectRatio)
? resolveFalCustomImageSize(aspectRatio, sizeBucket)
: "auto")
);
: "auto");
return constrainFalImageSize(resolvedSize, {
maxEdge: GPT_IMAGE_2_MAX_EDGE,
maxPixels: GPT_IMAGE_2_MAX_PIXELS,
});
}
function getFalImageSizeLabel(size: FalImageSize) {

View File

@ -1,6 +1,7 @@
import "server-only";
import sharp from "sharp";
import { resolveLongestEdgeDimensions } from "@/lib/ai/image-size-constraints";
import {
getKreaModelPresetById,
inferKreaModelKind,
@ -258,10 +259,6 @@ function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
}
function roundToMultiple(value: number, multiple = 16) {
return Math.max(multiple, Math.round(value / multiple) * multiple);
}
function normalizeImageSizeLabel(imageSize?: string): "1K" | "2K" | "4K" {
const normalized = normalizeText(imageSize).toUpperCase();
if (normalized === "1K" || normalized === "2K" || normalized === "4K") {
@ -274,21 +271,17 @@ function resolveImageDimensions(aspectRatio?: string, imageSize?: string) {
const size = normalizeImageSizeLabel(imageSize);
const base = size === "4K" ? 2048 : size === "2K" ? 1536 : 1024;
const ratio = normalizeAspectRatio(aspectRatio, "1:1");
if (ratio === "auto" || !ratio.includes(":")) {
return { width: base, height: base, imageSize: size };
}
const [rawW, rawH] = ratio.split(":").map((item) => Number(item));
const ratioW = Number.isFinite(rawW) && rawW > 0 ? rawW : 1;
const ratioH = Number.isFinite(rawH) && rawH > 0 ? rawH : 1;
const landscape = ratioW >= ratioH;
const width = landscape ? base : Math.round((base * ratioW) / ratioH);
const height = landscape ? Math.round((base * ratioH) / ratioW) : base;
return {
width: clamp(roundToMultiple(width), 256, 2048),
height: clamp(roundToMultiple(height), 256, 2048),
imageSize: size,
};
const { width, height } = resolveLongestEdgeDimensions({
aspectRatio: ratio === "auto" ? "1:1" : ratio,
longestEdge: base,
constraints: {
maxEdge: 2048,
maxPixels: 2048 * 2048,
minEdge: 256,
multiple: 16,
},
});
return { width, height, imageSize: size };
}
function resolveVideoDimensions(ratio?: string, resolution?: string) {

View File

@ -1,6 +1,9 @@
import "server-only";
import sharp from "sharp";
import {
resolveLongestEdgeDimensions,
} from "@/lib/ai/image-size-constraints";
import { uploadFile } from "@/lib/storage/service";
interface OpenAIImageConfig {
@ -124,10 +127,6 @@ function ceilToMultiple(value: number, multiple: number) {
return Math.max(multiple, Math.ceil(value / multiple) * multiple);
}
function floorToMultiple(value: number, multiple: number) {
return Math.max(multiple, Math.floor(value / multiple) * multiple);
}
function inferAspectRatioFromPrompt(prompt: string) {
const normalized = String(prompt || "").replace(/:/g, ":");
const match = normalized.match(/(\d{1,2})\s*(?::|比)\s*(\d{1,2})/);
@ -212,34 +211,15 @@ function resolveComflyGptImageSize(aspectRatio: string, imageSize: "1K" | "2K" |
"2K": 2048,
"4K": COMFLY_GPT_IMAGE_MAX_EDGE,
};
const ratioPair = parseAspectRatioPair(aspectRatio) || { width: 1, height: 1 };
const longestEdge = edgeMap[imageSize] || edgeMap["2K"];
const scale = longestEdge / Math.max(ratioPair.width, ratioPair.height);
let width = floorToMultiple(ratioPair.width * scale, OPENAI_IMAGE_DIMENSION_MULTIPLE);
let height = floorToMultiple(ratioPair.height * scale, OPENAI_IMAGE_DIMENSION_MULTIPLE);
while (
width > COMFLY_GPT_IMAGE_MAX_EDGE ||
height > COMFLY_GPT_IMAGE_MAX_EDGE ||
width * height > COMFLY_GPT_IMAGE_MAX_TOTAL_PIXELS
) {
const scaleDown = Math.min(
COMFLY_GPT_IMAGE_MAX_EDGE / width,
COMFLY_GPT_IMAGE_MAX_EDGE / height,
Math.sqrt(COMFLY_GPT_IMAGE_MAX_TOTAL_PIXELS / (width * height)),
1
);
const nextWidth = floorToMultiple(width * scaleDown, OPENAI_IMAGE_DIMENSION_MULTIPLE);
const nextHeight = floorToMultiple(height * scaleDown, OPENAI_IMAGE_DIMENSION_MULTIPLE);
if (nextWidth === width && nextHeight === height) {
if (width >= height) width -= OPENAI_IMAGE_DIMENSION_MULTIPLE;
else height -= OPENAI_IMAGE_DIMENSION_MULTIPLE;
} else {
width = nextWidth;
height = nextHeight;
}
}
const { width, height } = resolveLongestEdgeDimensions({
aspectRatio,
longestEdge: edgeMap[imageSize] || edgeMap["2K"],
constraints: {
maxEdge: COMFLY_GPT_IMAGE_MAX_EDGE,
maxPixels: COMFLY_GPT_IMAGE_MAX_TOTAL_PIXELS,
multiple: OPENAI_IMAGE_DIMENSION_MULTIPLE,
},
});
return `${width}x${height}`;
}

View File

@ -1,6 +1,7 @@
import "server-only";
import sharp from "sharp";
import { constrainImageDimensions } from "@/lib/ai/image-size-constraints";
import { uploadFile } from "@/lib/storage/service";
interface StepfunImageConfig {
@ -60,7 +61,17 @@ function parseAspectRatioValue(ratio?: string | null) {
function resolveStepfunSize(aspectRatio?: string, explicitSize?: string) {
const normalizedExplicit = String(explicitSize || "").trim();
if (/^\d{3,4}x\d{3,4}$/i.test(normalizedExplicit)) {
return normalizedExplicit;
const [rawWidth, rawHeight] = normalizedExplicit.split("x").map((item) => Number(item));
const { width, height } = constrainImageDimensions(
{ width: rawWidth, height: rawHeight },
{
maxEdge: 1360,
maxPixels: 1360 * 1360,
minEdge: 256,
multiple: 8,
}
);
return `${width}x${height}`;
}
const ratio = parseAspectRatioValue(aspectRatio);

View File

@ -1,5 +1,6 @@
import "server-only";
import { constrainImageDimensions } from "@/lib/ai/image-size-constraints";
import { uploadFile } from "@/lib/storage/service";
import { getDefaultStorageConfig } from "@/lib/db/queries";
import { createLogger } from "@/lib/observability/logger";
@ -23,6 +24,9 @@ const VOLCANO_IMAGE_FETCH_TIMEOUT_MS = 20_000;
const VOLCANO_INLINE_MAX_SIDE = 1024;
const DEFAULT_VOLCANO_IMAGE_SIZE = "2K";
const SEEDREAM_MODEL_ID_PATTERN = /doubao-seedream-4-5/i;
const VOLCANO_MAX_OUTPUT_EDGE = 6240;
const VOLCANO_MAX_OUTPUT_PIXELS = 4096 * 4096;
const VOLCANO_OUTPUT_SIZE_MULTIPLE = 16;
const VOLCANO_ASPECT_SIZE_OPTIONS: Record<
VolcanoImageSizeBucket,
@ -112,7 +116,8 @@ function normalizeVolcanoExactSize(imageSize?: string): string | null {
return null;
}
return `${width}x${height}`;
const constrained = constrainVolcanoOutputDimensions({ width, height });
return `${constrained.width}x${constrained.height}`;
}
function parseVolcanoAspectRatio(aspectRatio?: string): number | null {
@ -153,7 +158,17 @@ function resolveVolcanoAspectAwareSize(
return currentBest;
}, candidates[0]);
return `${best.width}x${best.height}`;
const constrained = constrainVolcanoOutputDimensions(best);
return `${constrained.width}x${constrained.height}`;
}
function constrainVolcanoOutputDimensions(dimensions: { width: number; height: number }) {
return constrainImageDimensions(dimensions, {
maxEdge: VOLCANO_MAX_OUTPUT_EDGE,
maxPixels: VOLCANO_MAX_OUTPUT_PIXELS,
minEdge: 256,
multiple: VOLCANO_OUTPUT_SIZE_MULTIPLE,
});
}
function getFirstImageSource(image: string | string[] | undefined): string | null {

View File

@ -341,6 +341,29 @@ function isCanvasImageSizeSupported(
return true;
}
function pickSupportedCanvasImageSize(
config: CanvasImageGenConfig | null | undefined,
requestedSize: CanvasImageSizeValue
): CanvasImageSizeValue {
if (isCanvasImageSizeSupported(config, requestedSize)) return requestedSize;
const preferredFallbacks: Record<CanvasImageSizeValue, CanvasImageSizeValue[]> = {
"4K": ["2K", "1K"],
"2K": ["1K"],
"1K": ["2K", "4K"],
};
return (
preferredFallbacks[requestedSize].find((candidate) =>
isCanvasImageSizeSupported(config, candidate)
) ||
(["1K", "2K", "4K"] as const).find((candidate) =>
isCanvasImageSizeSupported(config, candidate)
) ||
"2K"
);
}
function normalizeCanvasImageParamsByModel(params: {
imageCount: number;
imageSize: string;
@ -353,13 +376,8 @@ function normalizeCanvasImageParamsByModel(params: {
: 12;
const imageCount = Math.max(1, Math.min(maxImages, Math.floor(params.imageCount)));
let imageSize = normalizeCanvasImageSizeValue(params.imageSize);
if (!isCanvasImageSizeSupported(params.imageGenConfig, imageSize)) {
imageSize =
(["2K", "1K", "4K"] as const).find((candidate) =>
isCanvasImageSizeSupported(params.imageGenConfig, candidate)
) || "2K";
}
const requestedSize = normalizeCanvasImageSizeValue(params.imageSize);
const imageSize = pickSupportedCanvasImageSize(params.imageGenConfig, requestedSize);
return { imageCount, imageSize };
}