fix: map grsai gpt image sizes
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-11 21:03:53 +08:00
parent dd4426ded7
commit 2cf2162fba
4 changed files with 123 additions and 6 deletions

View File

@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Link2, Scan, SlidersHorizontal, Sparkles } from "lucide-react";
import { getGrsaiGptImageOutputSize } from "@/lib/ai/grsai-gpt-image-sizes";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
@ -204,6 +205,10 @@ export function ImageParamsBar({
});
const getComputedOutputSize = (aspectRatio: string, imageSize: string) => {
if (providerType === "grsai-gpt-image") {
return getGrsaiGptImageOutputSize(aspectRatio, imageSize);
}
const size = IMAGE_SIZE_PIXEL_MAP[imageSize] || IMAGE_SIZE_PIXEL_MAP["2K"];
if (aspectRatio === "auto" || aspectRatio === "1:1") {
return { width: size, height: size };

View File

@ -781,14 +781,24 @@ function inferExplicitAspectRatioFromText(text: string): string | undefined {
return undefined;
}
function inferExplicitImageSizeFromText(text: string): "1K" | "2K" | "4K" | undefined {
const normalized = String(text || "").toLowerCase();
if (/(^|[^0-9a-z])4\s*k([^0-9a-z]|$)/.test(normalized)) return "4K";
if (/(^|[^0-9a-z])2\s*k([^0-9a-z]|$)/.test(normalized)) return "2K";
if (/(^|[^0-9a-z])1\s*k([^0-9a-z]|$)/.test(normalized)) return "1K";
return undefined;
}
function extractExplicitImageParamsFromText(
text: string,
): Pick<AgentRecommendedImageParams, "aspectRatio" | "imageCount"> {
const output: Pick<AgentRecommendedImageParams, "aspectRatio" | "imageCount"> = {};
): Pick<AgentRecommendedImageParams, "aspectRatio" | "imageCount" | "imageSize"> {
const output: Pick<AgentRecommendedImageParams, "aspectRatio" | "imageCount" | "imageSize"> = {};
const aspectRatio = inferExplicitAspectRatioFromText(text);
const imageCount = inferExplicitImageCountFromText(text);
const imageSize = inferExplicitImageSizeFromText(text);
if (aspectRatio) output.aspectRatio = aspectRatio;
if (imageCount) output.imageCount = imageCount;
if (imageSize) output.imageSize = imageSize;
return output;
}
@ -1050,8 +1060,9 @@ function heuristicImageParams(
analysis: AnalysisResult,
): AgentRecommendedImageParams | undefined {
const normalized = text.toLowerCase();
const explicitImageSize = inferExplicitImageSizeFromText(text);
const params: AgentRecommendedImageParams = {
imageSize: "2K",
imageSize: explicitImageSize || (analysis.intent === "image_edit" ? "4K" : "2K"),
promptExtend: analysis.intent === "image_edit" ? false : true,
};

View File

@ -0,0 +1,94 @@
export type GrsaiGptImageSizeBucket = "1K" | "2K" | "4K";
type GrsaiGptImageSizeOption = {
bucket: GrsaiGptImageSizeBucket;
width: number;
height: number;
};
const GRSAI_GPT_IMAGE_SIZE_OPTIONS: GrsaiGptImageSizeOption[] = [
{ bucket: "1K", width: 1024, height: 1024 },
{ bucket: "1K", width: 1774, height: 887 },
{ bucket: "1K", width: 887, height: 1774 },
{ bucket: "1K", width: 1536, height: 1024 },
{ bucket: "1K", width: 1024, height: 1536 },
{ bucket: "2K", width: 2048, height: 2048 },
{ bucket: "2K", width: 2048, height: 1152 },
{ bucket: "2K", width: 1152, height: 2048 },
{ bucket: "2K", width: 2048, height: 1360 },
{ bucket: "2K", width: 1360, height: 2048 },
{ bucket: "2K", width: 2048, height: 880 },
{ bucket: "2K", width: 880, height: 2048 },
{ bucket: "2K", width: 2048, height: 688 },
{ bucket: "2K", width: 688, height: 2048 },
{ bucket: "2K", width: 2048, height: 1024 },
{ bucket: "2K", width: 1024, height: 2048 },
{ bucket: "4K", width: 2880, height: 2880 },
{ bucket: "4K", width: 3840, height: 2160 },
{ bucket: "4K", width: 2160, height: 3840 },
{ bucket: "4K", width: 3504, height: 2336 },
{ bucket: "4K", width: 2336, height: 3504 },
{ bucket: "4K", width: 3840, height: 1648 },
{ bucket: "4K", width: 1648, height: 3840 },
{ bucket: "4K", width: 3840, height: 1280 },
{ bucket: "4K", width: 1280, height: 3840 },
{ bucket: "4K", width: 3840, height: 1920 },
{ bucket: "4K", width: 1920, height: 3840 },
];
const GRSAI_GPT_IMAGE_SIZE_VALUES = new Set(
GRSAI_GPT_IMAGE_SIZE_OPTIONS.map((option) => `${option.width}x${option.height}`)
);
function normalizeGrsaiGptImageBucket(imageSize?: string): GrsaiGptImageSizeBucket {
const normalized = String(imageSize || "").trim().toUpperCase();
if (normalized === "1K" || normalized === "2K" || normalized === "4K") {
return normalized;
}
return "2K";
}
function parseAspectRatioValue(aspectRatio?: string): number {
const normalized = String(aspectRatio || "").trim().toLowerCase();
if (!normalized || normalized === "auto") {
return 1;
}
const [widthRaw, heightRaw] = normalized.split(":").map((part) => Number(part));
if (!Number.isFinite(widthRaw) || !Number.isFinite(heightRaw) || widthRaw <= 0 || heightRaw <= 0) {
return 1;
}
return widthRaw / heightRaw;
}
export function getGrsaiGptImageOutputSize(aspectRatio?: string, imageSize?: string) {
const bucket = normalizeGrsaiGptImageBucket(imageSize);
const targetRatio = parseAspectRatioValue(aspectRatio);
const candidates = GRSAI_GPT_IMAGE_SIZE_OPTIONS.filter((option) => option.bucket === bucket);
const best = candidates.reduce((currentBest, option) => {
const currentDelta = Math.abs(currentBest.width / currentBest.height - targetRatio);
const optionDelta = Math.abs(option.width / option.height - targetRatio);
if (optionDelta < currentDelta) {
return option;
}
if (optionDelta === currentDelta) {
const currentArea = currentBest.width * currentBest.height;
const optionArea = option.width * option.height;
return optionArea > currentArea ? option : currentBest;
}
return currentBest;
}, candidates[0]);
return { width: best.width, height: best.height };
}
export function resolveGrsaiGptImageSize(aspectRatio?: string, imageSize?: string): string {
const requestedSize = String(imageSize || "").trim().toLowerCase();
if (/^\d+x\d+$/.test(requestedSize) && GRSAI_GPT_IMAGE_SIZE_VALUES.has(requestedSize)) {
return requestedSize;
}
const { width, height } = getGrsaiGptImageOutputSize(aspectRatio, imageSize);
return `${width}x${height}`;
}

View File

@ -1,5 +1,6 @@
import "server-only";
import { resolveGrsaiGptImageSize } from "@/lib/ai/grsai-gpt-image-sizes";
import { uploadFile } from "@/lib/storage/service";
interface GeminiImageConfig {
@ -482,8 +483,11 @@ async function sendGeminiRequest(
}
if (isGptImageModel) {
requestBody.size = imageParams?.aspectRatio || "auto";
requestBody.size = resolveGrsaiGptImageSize(imageParams?.aspectRatio, imageParams?.imageSize);
requestBody.shutProgress = false;
console.log(
`${apiLogPrefix} Resolved size: aspectRatio=${imageParams?.aspectRatio || "auto"}, imageSize=${imageParams?.imageSize || "2K"} => ${requestBody.size}`
);
} else {
// 添加 Nano Banana 特定参数
if (imageParams?.aspectRatio) {
@ -602,8 +606,11 @@ async function sendGeminiRequest(
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
console.error("[Gemini Image] Request timeout after 120 seconds");
throw new Error("Gemini 请求超时(120秒),将自动切换备用模型");
const timeoutSeconds = Math.round(timeout / 1000);
console.error(`${apiLogPrefix} Request timeout after ${timeoutSeconds} seconds`);
throw new Error(
`${isGptImageModel ? "Grsai GPT Image" : "Gemini"} 请求超时(${timeoutSeconds}秒),将自动切换备用模型`
);
}
throw error;
}