fix: adapt fal image sizing parameters
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-08 11:17:05 +08:00
parent c4c0bd8f64
commit b64a512dfb

View File

@ -59,6 +59,34 @@ const GPT_IMAGE_2_EDIT_IMAGE_SIZE_PRESETS = new Set([
"landscape_16_9",
]);
const GPT_IMAGE_2_EDIT_QUALITIES = new Set(["low", "medium", "high"]);
const FAL_ASPECT_RATIO_VALUES = new Set([
"21:9",
"16:9",
"3:2",
"4:3",
"5:4",
"1:1",
"4:5",
"3:4",
"2:3",
"9:16",
"9:21",
]);
const FAL_IMAGE_SIZE_PRESETS = new Set([
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
]);
const FAL_IMAGE_SIZE_BY_ASPECT_RATIO: Record<string, string> = {
"1:1": "square_hd",
"4:3": "landscape_4_3",
"3:4": "portrait_4_3",
"16:9": "landscape_16_9",
"9:16": "portrait_16_9",
};
type FalImageSize = string | { width: number; height: number };
@ -172,6 +200,7 @@ function normalizeAspectRatio(aspectRatio?: string) {
"3:4",
"2:3",
"9:16",
"9:21",
"4:1",
"1:4",
"8:1",
@ -191,6 +220,82 @@ function normalizeResolution(imageSize?: string) {
return DEFAULT_RESOLUTION;
}
function normalizeExplicitAspectRatio(aspectRatio?: string) {
const normalized = normalizeAspectRatio(aspectRatio);
if (normalized === "auto") return undefined;
return FAL_ASPECT_RATIO_VALUES.has(normalized) ? normalized : undefined;
}
function normalizeAnyExplicitAspectRatio(aspectRatio?: string) {
const normalized = normalizeAspectRatio(aspectRatio);
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 targetArea = areaByBucket[bucket] || areaByBucket["1K"];
const width = roundToMultiple(Math.sqrt(targetArea * ratio), 16);
const height = roundToMultiple(width / ratio, 16);
return { width, height };
}
function normalizeFalImageSize(imageSize?: string, aspectRatio?: string): FalImageSize {
const rawSize = String(imageSize || "").trim();
const normalizedSize = rawSize.toLowerCase().replace(/\s+/g, "");
if (FAL_IMAGE_SIZE_PRESETS.has(normalizedSize)) {
return normalizedSize;
}
const customSizeMatch = normalizedSize.match(/^(\d{2,5})[x×](\d{2,5})$/i);
if (customSizeMatch) {
const width = Number(customSizeMatch[1]);
const height = Number(customSizeMatch[2]);
if (
Number.isFinite(width) &&
Number.isFinite(height) &&
width >= 256 &&
height >= 256 &&
width <= 4096 &&
height <= 4096
) {
return {
width: roundToMultiple(width, 16),
height: roundToMultiple(height, 16),
};
}
}
const normalizedAspectRatio = normalizeAnyExplicitAspectRatio(aspectRatio);
if (normalizedAspectRatio && FAL_IMAGE_SIZE_BY_ASPECT_RATIO[normalizedAspectRatio]) {
return FAL_IMAGE_SIZE_BY_ASPECT_RATIO[normalizedAspectRatio];
}
return normalizedAspectRatio
? resolveFalCustomImageSize(normalizedAspectRatio, rawSize)
: "square_hd";
}
function normalizeOutputFormat(outputFormat?: string) {
if (!outputFormat) return DEFAULT_OUTPUT_FORMAT;
const normalized = outputFormat.trim().toLowerCase();
@ -228,6 +333,31 @@ function normalizeFalEndpointId(modelId: string, hasInputImages: boolean) {
return endpoint;
}
function isFalNanoBananaModelId(modelId: string) {
const normalized = modelId.trim().toLowerCase().replace(/\/edit$/, "");
return FAL_NANO_BANANA_MODEL_IDS.has(normalized);
}
function isFalFluxKontextDevModelId(modelId: string) {
return modelId.trim().toLowerCase().replace(/\/$/, "") === "fal-ai/flux-kontext/dev";
}
function isFalKontextModelId(modelId: string) {
return modelId.trim().toLowerCase().includes("kontext");
}
function shouldUseFalImageSize(modelId: string, hasInputImages: boolean) {
if (hasInputImages) return false;
const normalized = modelId.trim().toLowerCase();
if (isFalNanoBananaModelId(normalized) || isFalKontextModelId(normalized)) return false;
return (
normalized.startsWith("fal-ai/flux/") ||
normalized.startsWith("fal-ai/flux-lora") ||
normalized.includes("/flux/dev") ||
normalized.includes("/flux/schnell")
);
}
function normalizeGptImage2EditQuality(value?: string) {
if (!value) return "high";
const normalized = value.trim().toLowerCase();
@ -266,7 +396,12 @@ function normalizeGptImage2EditImageSize(
const sizeBucket = rawSize.toUpperCase();
const ratio = normalizeAspectRatio(aspectRatio).toLowerCase();
return GPT_IMAGE_2_EDIT_SIZE_BY_BUCKET[sizeBucket]?.[ratio] || "auto";
return (
GPT_IMAGE_2_EDIT_SIZE_BY_BUCKET[sizeBucket]?.[ratio] ||
(normalizeAnyExplicitAspectRatio(aspectRatio)
? resolveFalCustomImageSize(aspectRatio, sizeBucket)
: "auto")
);
}
function getFalImageSizeLabel(size: FalImageSize) {
@ -519,24 +654,47 @@ function buildFalRequestPayload(params: {
input.openai_api_key = openaiApiKey;
}
} else {
const explicitAspectRatio = normalizeExplicitAspectRatio(aspectRatio);
input = {
prompt,
num_images: imageCount,
aspect_ratio: aspectRatio,
resolution,
output_format: outputFormat,
safety_tolerance: safetyTolerance,
limit_generations: true,
};
if (imageUrls.length > 0) {
input.image_urls = imageUrls;
if (isFalNanoBananaModelId(falEndpointId)) {
input.aspect_ratio = aspectRatio;
input.resolution = resolution;
input.limit_generations = true;
} else if (isFalFluxKontextDevModelId(falEndpointId) && imageUrls.length > 0) {
input.image_url = imageUrls[0];
input.enable_safety_checker = true;
input.resolution_mode = explicitAspectRatio || "match_input";
} else if (isFalKontextModelId(falEndpointId)) {
if (explicitAspectRatio) {
input.aspect_ratio = explicitAspectRatio;
}
if (imageUrls.length > 0) {
input.image_url = imageUrls[0];
if (imageUrls.length > 1 || falEndpointId.toLowerCase().includes("/multi")) {
input.image_urls = imageUrls;
}
}
} else if (shouldUseFalImageSize(falEndpointId, imageUrls.length > 0)) {
input.image_size = normalizeFalImageSize(params.imageParams?.imageSize, aspectRatio);
input.enable_safety_checker = true;
} else if (explicitAspectRatio) {
input.aspect_ratio = explicitAspectRatio;
}
if (seed !== undefined) {
input.seed = seed;
}
if (imageUrls.length > 0 && !input.image_url && !input.image_urls) {
input.image_urls = imageUrls;
}
if (typeof params.imageParams?.enableWebSearch === "boolean") {
input.enable_web_search = params.imageParams.enableWebSearch;
}
@ -606,6 +764,7 @@ export async function submitFalImageTask(params: {
imageCount: request.imageCount,
hasInputImages: request.imageUrls.length > 0,
resolution: request.outputSizeLabel,
inputKeys: Object.keys(request.input),
});
const enqueueResult = await fal.queue.submit(request.falEndpointId, {
@ -680,6 +839,7 @@ export function createFalImage({ apiKey, baseURL }: FalImageConfig) {
aspectRatio: request.aspectRatio,
resolution: request.outputSizeLabel,
hasInputImages: request.imageUrls.length > 0,
inputKeys: Object.keys(request.input),
});
const fal = await createFalClientFromApiKey(apiKey);