fix: infer SnapGen auto aspect ratio

This commit is contained in:
134 2026-06-24 11:35:41 +08:00
parent ed2c64d2a6
commit dcd005d369
2 changed files with 174 additions and 14 deletions

View File

@ -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,

View File

@ -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[] = [
{