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 sharp from "sharp";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
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 {
|
interface SnapgenImageConfig {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@ -23,6 +27,7 @@ interface SnapgenImageModelConfig {
|
|||||||
supports1K?: boolean;
|
supports1K?: boolean;
|
||||||
supports2K?: boolean;
|
supports2K?: boolean;
|
||||||
supports4K?: boolean;
|
supports4K?: boolean;
|
||||||
|
supportedAspectRatios?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadedImage {
|
interface UploadedImage {
|
||||||
@ -71,16 +76,6 @@ function filterSourceImages(urls: string[], sourceUrls: string[]) {
|
|||||||
return urls.filter((url) => !sourceSet.has(normalizeUrlForCompare(url)));
|
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) {
|
function normalizeResolution(imageSize?: string, apiFormat?: SnapgenApiFormat) {
|
||||||
const defaultResolution = apiFormat === "snapgen-gpt-image-2" ? "2K" : "1K";
|
const defaultResolution = apiFormat === "snapgen-gpt-image-2" ? "2K" : "1K";
|
||||||
if (!imageSize) return defaultResolution;
|
if (!imageSize) return defaultResolution;
|
||||||
@ -108,6 +103,157 @@ function normalizeOrientation(aspectRatio: string): "landscape" | "portrait" | "
|
|||||||
return "landscape";
|
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" {
|
function normalizeGrokMode(value?: string): "SPEED" | "QUALITY" {
|
||||||
const normalized = String(value || "").trim().toUpperCase();
|
const normalized = String(value || "").trim().toUpperCase();
|
||||||
return normalized === "QUALITY" ? "QUALITY" : "SPEED";
|
return normalized === "QUALITY" ? "QUALITY" : "SPEED";
|
||||||
@ -709,7 +855,13 @@ export function createSnapgenImage({ apiKey, baseURL }: SnapgenImageConfig) {
|
|||||||
const requestedModelId = normalizeSnapgenModelId(options.overrideModelId || modelId);
|
const requestedModelId = normalizeSnapgenModelId(options.overrideModelId || modelId);
|
||||||
const runtimeModelId = resolveSnapgenRuntimeModelId(requestedModelId);
|
const runtimeModelId = resolveSnapgenRuntimeModelId(requestedModelId);
|
||||||
const apiFormat = inferApiFormat(runtimeModelId);
|
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 resolution = normalizeResolution(options.imageParams?.imageSize, apiFormat);
|
||||||
const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat);
|
const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat);
|
||||||
const maxImages = options.imageGenConfig?.maxImages || (apiFormat === "snapgen-grok-image" ? 8 : apiFormat === "snapgen-meta-ai-image" ? 4 : 1);
|
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:", {
|
console.log("[SnapGen Image] Creating task:", {
|
||||||
model: runtimeModelId,
|
model: runtimeModelId,
|
||||||
apiFormat,
|
apiFormat,
|
||||||
|
requestedAspectRatio: resolvedAspectRatio.requestedAspectRatio,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
|
aspectRatioSource: resolvedAspectRatio.aspectRatioSource,
|
||||||
|
referenceImageSize: resolvedAspectRatio.referenceImageSize,
|
||||||
resolution,
|
resolution,
|
||||||
imageCount,
|
imageCount,
|
||||||
referenceImageCount: imageUrls.length,
|
referenceImageCount: imageUrls.length,
|
||||||
|
|||||||
@ -32,8 +32,8 @@ const SNAPGEN_COMMON_IMAGE_CONFIG = {
|
|||||||
loadingColor: "blue",
|
loadingColor: "blue",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SNAPGEN_STANDARD_ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"];
|
export const SNAPGEN_STANDARD_REQUEST_ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"];
|
||||||
const SNAPGEN_GPT_IMAGE_2_ASPECT_RATIOS = [
|
export const SNAPGEN_GPT_IMAGE_2_REQUEST_ASPECT_RATIOS = [
|
||||||
"1:1",
|
"1:1",
|
||||||
"16:9",
|
"16:9",
|
||||||
"9:16",
|
"9:16",
|
||||||
@ -43,6 +43,11 @@ const SNAPGEN_GPT_IMAGE_2_ASPECT_RATIOS = [
|
|||||||
"3:2",
|
"3:2",
|
||||||
"2:3",
|
"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[] = [
|
export const SNAPGEN_IMAGE_MODEL_PRESETS: SnapgenImageModelPreset[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user