fix: infer SnapGen auto aspect ratio
This commit is contained in:
parent
ed2c64d2a6
commit
dcd005d369
@ -2,7 +2,11 @@ import "server-only";
|
||||
|
||||
import sharp from "sharp";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
import { resolveSnapgenImageRuntimeModelId } from "@/lib/ai/snapgen-image-models";
|
||||
import {
|
||||
SNAPGEN_GPT_IMAGE_2_REQUEST_ASPECT_RATIOS,
|
||||
SNAPGEN_STANDARD_REQUEST_ASPECT_RATIOS,
|
||||
resolveSnapgenImageRuntimeModelId,
|
||||
} from "@/lib/ai/snapgen-image-models";
|
||||
|
||||
interface SnapgenImageConfig {
|
||||
apiKey: string;
|
||||
@ -23,6 +27,7 @@ interface SnapgenImageModelConfig {
|
||||
supports1K?: boolean;
|
||||
supports2K?: boolean;
|
||||
supports4K?: boolean;
|
||||
supportedAspectRatios?: string[];
|
||||
}
|
||||
|
||||
interface UploadedImage {
|
||||
@ -71,16 +76,6 @@ function filterSourceImages(urls: string[], sourceUrls: string[]) {
|
||||
return urls.filter((url) => !sourceSet.has(normalizeUrlForCompare(url)));
|
||||
}
|
||||
|
||||
function normalizeAspectRatio(aspectRatio?: string) {
|
||||
if (!aspectRatio) return "1:1";
|
||||
const ratio = aspectRatio.trim();
|
||||
if (ratio.toLowerCase() === "auto") return "1:1";
|
||||
if (["1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "3:2", "2:3"].includes(ratio)) {
|
||||
return ratio;
|
||||
}
|
||||
return "1:1";
|
||||
}
|
||||
|
||||
function normalizeResolution(imageSize?: string, apiFormat?: SnapgenApiFormat) {
|
||||
const defaultResolution = apiFormat === "snapgen-gpt-image-2" ? "2K" : "1K";
|
||||
if (!imageSize) return defaultResolution;
|
||||
@ -108,6 +103,157 @@ function normalizeOrientation(aspectRatio: string): "landscape" | "portrait" | "
|
||||
return "landscape";
|
||||
}
|
||||
|
||||
function normalizeAspectRatioLabel(value?: string) {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
if (!normalized) return "";
|
||||
if (normalized === "auto") return "auto";
|
||||
const match = normalized.match(/^(\d+)\s*:\s*(\d+)$/);
|
||||
if (!match) return "";
|
||||
return `${Number(match[1])}:${Number(match[2])}`;
|
||||
}
|
||||
|
||||
function parseAspectRatioValue(aspectRatio: string) {
|
||||
const normalized = normalizeAspectRatioLabel(aspectRatio);
|
||||
const match = normalized.match(/^(\d+):(\d+)$/);
|
||||
if (!match) return null;
|
||||
const width = Number(match[1]);
|
||||
const height = Number(match[2]);
|
||||
if (!width || !height) return null;
|
||||
return width / height;
|
||||
}
|
||||
|
||||
function getFallbackRequestAspectRatios(apiFormat: SnapgenApiFormat) {
|
||||
if (apiFormat === "snapgen-gpt-image-2") {
|
||||
return SNAPGEN_GPT_IMAGE_2_REQUEST_ASPECT_RATIOS;
|
||||
}
|
||||
return SNAPGEN_STANDARD_REQUEST_ASPECT_RATIOS;
|
||||
}
|
||||
|
||||
function getRequestAspectRatios(
|
||||
apiFormat: SnapgenApiFormat,
|
||||
configuredAspectRatios?: string[]
|
||||
) {
|
||||
const configured = Array.isArray(configuredAspectRatios)
|
||||
? configuredAspectRatios
|
||||
.map((ratio) => normalizeAspectRatioLabel(ratio))
|
||||
.filter((ratio) => ratio && ratio !== "auto" && parseAspectRatioValue(ratio) !== null)
|
||||
: [];
|
||||
|
||||
if (configured.length > 0) {
|
||||
return Array.from(new Set(configured));
|
||||
}
|
||||
|
||||
return getFallbackRequestAspectRatios(apiFormat);
|
||||
}
|
||||
|
||||
function getDefaultRequestAspectRatio(supportedAspectRatios: string[]) {
|
||||
return supportedAspectRatios.includes("1:1")
|
||||
? "1:1"
|
||||
: supportedAspectRatios[0] || "1:1";
|
||||
}
|
||||
|
||||
function findClosestAspectRatio(targetAspect: number, supportedAspectRatios: string[]) {
|
||||
const fallback = getDefaultRequestAspectRatio(supportedAspectRatios);
|
||||
if (!Number.isFinite(targetAspect) || targetAspect <= 0) return fallback;
|
||||
|
||||
let closest = fallback;
|
||||
let closestScore = Number.POSITIVE_INFINITY;
|
||||
for (const aspectRatio of supportedAspectRatios) {
|
||||
const aspectValue = parseAspectRatioValue(aspectRatio);
|
||||
if (!aspectValue) continue;
|
||||
const score = Math.abs(Math.log(targetAspect / aspectValue));
|
||||
if (score < closestScore) {
|
||||
closest = aspectRatio;
|
||||
closestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
async function inferImageDimensionsFromUrl(
|
||||
imageUrl: string
|
||||
): Promise<{ width: number; height: number; aspectRatio: number } | null> {
|
||||
if (!/^https?:\/\//i.test(imageUrl)) return null;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 20_000);
|
||||
try {
|
||||
const response = await fetch(imageUrl, {
|
||||
cache: "no-store",
|
||||
redirect: "follow",
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
"User-Agent": "ai-chatbot-snapgen/1.0",
|
||||
},
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
if (!buffer.length) return null;
|
||||
|
||||
const metadata = await sharp(buffer, { failOn: "none" }).metadata();
|
||||
if (!metadata.width || !metadata.height || metadata.height <= 0) return null;
|
||||
|
||||
return {
|
||||
width: metadata.width,
|
||||
height: metadata.height,
|
||||
aspectRatio: metadata.width / metadata.height,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[SnapGen Image] Failed to infer reference aspect ratio:", error);
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAspectRatio({
|
||||
requestedAspectRatio,
|
||||
imageUrls,
|
||||
apiFormat,
|
||||
configuredAspectRatios,
|
||||
}: {
|
||||
requestedAspectRatio?: string;
|
||||
imageUrls: string[];
|
||||
apiFormat: SnapgenApiFormat;
|
||||
configuredAspectRatios?: string[];
|
||||
}) {
|
||||
const supportedAspectRatios = getRequestAspectRatios(apiFormat, configuredAspectRatios);
|
||||
const normalizedRequest = normalizeAspectRatioLabel(requestedAspectRatio) || "auto";
|
||||
const fallback = getDefaultRequestAspectRatio(supportedAspectRatios);
|
||||
|
||||
if (normalizedRequest !== "auto") {
|
||||
const aspectRatio = supportedAspectRatios.includes(normalizedRequest)
|
||||
? normalizedRequest
|
||||
: fallback;
|
||||
return {
|
||||
requestedAspectRatio: normalizedRequest,
|
||||
aspectRatio,
|
||||
aspectRatioSource: aspectRatio === normalizedRequest ? "manual" : "fallback",
|
||||
};
|
||||
}
|
||||
|
||||
const referenceImageUrl = imageUrls[0];
|
||||
if (referenceImageUrl) {
|
||||
const dimensions = await inferImageDimensionsFromUrl(referenceImageUrl);
|
||||
if (dimensions) {
|
||||
return {
|
||||
requestedAspectRatio: "auto",
|
||||
aspectRatio: findClosestAspectRatio(dimensions.aspectRatio, supportedAspectRatios),
|
||||
aspectRatioSource: "reference",
|
||||
referenceImageSize: `${dimensions.width}x${dimensions.height}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requestedAspectRatio: "auto",
|
||||
aspectRatio: fallback,
|
||||
aspectRatioSource: "fallback",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGrokMode(value?: string): "SPEED" | "QUALITY" {
|
||||
const normalized = String(value || "").trim().toUpperCase();
|
||||
return normalized === "QUALITY" ? "QUALITY" : "SPEED";
|
||||
@ -709,7 +855,13 @@ export function createSnapgenImage({ apiKey, baseURL }: SnapgenImageConfig) {
|
||||
const requestedModelId = normalizeSnapgenModelId(options.overrideModelId || modelId);
|
||||
const runtimeModelId = resolveSnapgenRuntimeModelId(requestedModelId);
|
||||
const apiFormat = inferApiFormat(runtimeModelId);
|
||||
const aspectRatio = normalizeAspectRatio(options.imageParams?.aspectRatio);
|
||||
const resolvedAspectRatio = await resolveAspectRatio({
|
||||
requestedAspectRatio: options.imageParams?.aspectRatio,
|
||||
imageUrls,
|
||||
apiFormat,
|
||||
configuredAspectRatios: options.imageGenConfig?.supportedAspectRatios,
|
||||
});
|
||||
const aspectRatio = resolvedAspectRatio.aspectRatio;
|
||||
const resolution = normalizeResolution(options.imageParams?.imageSize, apiFormat);
|
||||
const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat);
|
||||
const maxImages = options.imageGenConfig?.maxImages || (apiFormat === "snapgen-grok-image" ? 8 : apiFormat === "snapgen-meta-ai-image" ? 4 : 1);
|
||||
@ -729,7 +881,10 @@ export function createSnapgenImage({ apiKey, baseURL }: SnapgenImageConfig) {
|
||||
console.log("[SnapGen Image] Creating task:", {
|
||||
model: runtimeModelId,
|
||||
apiFormat,
|
||||
requestedAspectRatio: resolvedAspectRatio.requestedAspectRatio,
|
||||
aspectRatio,
|
||||
aspectRatioSource: resolvedAspectRatio.aspectRatioSource,
|
||||
referenceImageSize: resolvedAspectRatio.referenceImageSize,
|
||||
resolution,
|
||||
imageCount,
|
||||
referenceImageCount: imageUrls.length,
|
||||
|
||||
@ -32,8 +32,8 @@ const SNAPGEN_COMMON_IMAGE_CONFIG = {
|
||||
loadingColor: "blue",
|
||||
};
|
||||
|
||||
const SNAPGEN_STANDARD_ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"];
|
||||
const SNAPGEN_GPT_IMAGE_2_ASPECT_RATIOS = [
|
||||
export const SNAPGEN_STANDARD_REQUEST_ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"];
|
||||
export const SNAPGEN_GPT_IMAGE_2_REQUEST_ASPECT_RATIOS = [
|
||||
"1:1",
|
||||
"16:9",
|
||||
"9:16",
|
||||
@ -43,6 +43,11 @@ const SNAPGEN_GPT_IMAGE_2_ASPECT_RATIOS = [
|
||||
"3:2",
|
||||
"2:3",
|
||||
];
|
||||
const SNAPGEN_STANDARD_ASPECT_RATIOS = ["auto", ...SNAPGEN_STANDARD_REQUEST_ASPECT_RATIOS];
|
||||
const SNAPGEN_GPT_IMAGE_2_ASPECT_RATIOS = [
|
||||
"auto",
|
||||
...SNAPGEN_GPT_IMAGE_2_REQUEST_ASPECT_RATIOS,
|
||||
];
|
||||
|
||||
export const SNAPGEN_IMAGE_MODEL_PRESETS: SnapgenImageModelPreset[] = [
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user