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",
|
"landscape_16_9",
|
||||||
]);
|
]);
|
||||||
const GPT_IMAGE_2_EDIT_QUALITIES = new Set(["low", "medium", "high"]);
|
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 };
|
type FalImageSize = string | { width: number; height: number };
|
||||||
|
|
||||||
@ -172,6 +200,7 @@ function normalizeAspectRatio(aspectRatio?: string) {
|
|||||||
"3:4",
|
"3:4",
|
||||||
"2:3",
|
"2:3",
|
||||||
"9:16",
|
"9:16",
|
||||||
|
"9:21",
|
||||||
"4:1",
|
"4:1",
|
||||||
"1:4",
|
"1:4",
|
||||||
"8:1",
|
"8:1",
|
||||||
@ -191,6 +220,82 @@ function normalizeResolution(imageSize?: string) {
|
|||||||
return DEFAULT_RESOLUTION;
|
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) {
|
function normalizeOutputFormat(outputFormat?: string) {
|
||||||
if (!outputFormat) return DEFAULT_OUTPUT_FORMAT;
|
if (!outputFormat) return DEFAULT_OUTPUT_FORMAT;
|
||||||
const normalized = outputFormat.trim().toLowerCase();
|
const normalized = outputFormat.trim().toLowerCase();
|
||||||
@ -228,6 +333,31 @@ function normalizeFalEndpointId(modelId: string, hasInputImages: boolean) {
|
|||||||
return endpoint;
|
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) {
|
function normalizeGptImage2EditQuality(value?: string) {
|
||||||
if (!value) return "high";
|
if (!value) return "high";
|
||||||
const normalized = value.trim().toLowerCase();
|
const normalized = value.trim().toLowerCase();
|
||||||
@ -266,7 +396,12 @@ function normalizeGptImage2EditImageSize(
|
|||||||
|
|
||||||
const sizeBucket = rawSize.toUpperCase();
|
const sizeBucket = rawSize.toUpperCase();
|
||||||
const ratio = normalizeAspectRatio(aspectRatio).toLowerCase();
|
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) {
|
function getFalImageSizeLabel(size: FalImageSize) {
|
||||||
@ -519,24 +654,47 @@ function buildFalRequestPayload(params: {
|
|||||||
input.openai_api_key = openaiApiKey;
|
input.openai_api_key = openaiApiKey;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const explicitAspectRatio = normalizeExplicitAspectRatio(aspectRatio);
|
||||||
input = {
|
input = {
|
||||||
prompt,
|
prompt,
|
||||||
num_images: imageCount,
|
num_images: imageCount,
|
||||||
aspect_ratio: aspectRatio,
|
|
||||||
resolution,
|
|
||||||
output_format: outputFormat,
|
output_format: outputFormat,
|
||||||
safety_tolerance: safetyTolerance,
|
safety_tolerance: safetyTolerance,
|
||||||
limit_generations: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (imageUrls.length > 0) {
|
if (isFalNanoBananaModelId(falEndpointId)) {
|
||||||
input.image_urls = imageUrls;
|
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) {
|
if (seed !== undefined) {
|
||||||
input.seed = seed;
|
input.seed = seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageUrls.length > 0 && !input.image_url && !input.image_urls) {
|
||||||
|
input.image_urls = imageUrls;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof params.imageParams?.enableWebSearch === "boolean") {
|
if (typeof params.imageParams?.enableWebSearch === "boolean") {
|
||||||
input.enable_web_search = params.imageParams.enableWebSearch;
|
input.enable_web_search = params.imageParams.enableWebSearch;
|
||||||
}
|
}
|
||||||
@ -606,6 +764,7 @@ export async function submitFalImageTask(params: {
|
|||||||
imageCount: request.imageCount,
|
imageCount: request.imageCount,
|
||||||
hasInputImages: request.imageUrls.length > 0,
|
hasInputImages: request.imageUrls.length > 0,
|
||||||
resolution: request.outputSizeLabel,
|
resolution: request.outputSizeLabel,
|
||||||
|
inputKeys: Object.keys(request.input),
|
||||||
});
|
});
|
||||||
|
|
||||||
const enqueueResult = await fal.queue.submit(request.falEndpointId, {
|
const enqueueResult = await fal.queue.submit(request.falEndpointId, {
|
||||||
@ -680,6 +839,7 @@ export function createFalImage({ apiKey, baseURL }: FalImageConfig) {
|
|||||||
aspectRatio: request.aspectRatio,
|
aspectRatio: request.aspectRatio,
|
||||||
resolution: request.outputSizeLabel,
|
resolution: request.outputSizeLabel,
|
||||||
hasInputImages: request.imageUrls.length > 0,
|
hasInputImages: request.imageUrls.length > 0,
|
||||||
|
inputKeys: Object.keys(request.input),
|
||||||
});
|
});
|
||||||
|
|
||||||
const fal = await createFalClientFromApiKey(apiKey);
|
const fal = await createFalClientFromApiKey(apiKey);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user