fix: adapt fal image sizing parameters
This commit is contained in:
parent
c4c0bd8f64
commit
b64a512dfb
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user