From dcd005d369dc28af3da5be7d14fbb659ab3bb1ee Mon Sep 17 00:00:00 2001 From: 134 Date: Wed, 24 Jun 2026 11:35:41 +0800 Subject: [PATCH] fix: infer SnapGen auto aspect ratio --- lib/ai/providers/snapgen-image.ts | 179 ++++++++++++++++++++++++++++-- lib/ai/snapgen-image-models.ts | 9 +- 2 files changed, 174 insertions(+), 14 deletions(-) diff --git a/lib/ai/providers/snapgen-image.ts b/lib/ai/providers/snapgen-image.ts index 51f38da52..281a6c078 100644 --- a/lib/ai/providers/snapgen-image.ts +++ b/lib/ai/providers/snapgen-image.ts @@ -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, diff --git a/lib/ai/snapgen-image-models.ts b/lib/ai/snapgen-image-models.ts index 1f2dbcb68..091053cf4 100644 --- a/lib/ai/snapgen-image-models.ts +++ b/lib/ai/snapgen-image-models.ts @@ -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[] = [ {