From 81144ea009a91f8b81e6fb9d3901107b9b8ff912 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Wed, 8 Jul 2026 15:11:18 +0800 Subject: [PATCH] fix: resolve issue with Gemini image format not passing aspect ratio configuration --- CHANGELOG.md | 1 + docs/content/docs/progress/pending-test.mdx | 1 + web/src/services/api/image.ts | 41 ++++++++++++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4818574..f6f2c91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ + [新增] 新增Codex App插件支持。 + [新增] 配置与用户偏好新增独立页面和 Codex 连接配置 Tab。 + [修复] 修复生图工作台重试成功结果刷新后丢失的问题。 ++ [修复] 修复 Gemini 调用格式生图未传递尺寸比例配置的问题。 + [新增] 新增GitHub Pages 前端静态站点发布 workflow。 + [新增] 图片切图支持等分线直接拖拽调整,并可新增、删除和重置横向 / 纵向切图线。 diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 935b32a..64871aa 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -16,6 +16,7 @@ description: 当前版本已实现但仍需人工验证的变更项 - 画布 Agent:右侧 Agent 面板移除网站 / 本机模式切换,固定使用本机 Agent。 - 配置与用户偏好:每个模型渠道新增 OpenAI / Gemini 调用格式选择,默认 OpenAI,切换默认格式时同步更新默认 Base URL;渠道 Tab 增加紧凑醒目的提醒,说明模型是否可选需要到“模型”Tab 选择。 - Gemini 调用格式:支持拉取 Gemini 模型列表,并用于文本问答、在线 Agent 工具调用、基础生图和图生图请求。 +- Gemini 生图:生图工作台和画布生图会把尺寸配置转换为 Gemini `responseFormat.image.aspectRatio` 传入,需验证 1:1、9:16、16:9 及自定义像素尺寸的输出比例。 - 音频和视频生成:选择 Gemini 调用格式时先给出不支持提示,仍需使用 OpenAI 兼容渠道。 - WebDAV 同步:移除 Next.js 转发代理和连接方式选择,改为浏览器前端直接连接 WebDAV 服务。 - 提示词库:移除 Next.js route 抓取和服务端内存缓存,改为浏览器前端直连 GitHub 原始文件并缓存到 IndexedDB。 diff --git a/web/src/services/api/image.ts b/web/src/services/api/image.ts index fe2fa02..bd326a5 100644 --- a/web/src/services/api/image.ts +++ b/web/src/services/api/image.ts @@ -112,6 +112,9 @@ const IMAGE_MAX_EDGE = 3840; const IMAGE_MAX_RATIO = 3; const IMAGE_OUTPUT_FORMAT = "png"; +const GEMINI_SUPPORTED_RATIOS = ["1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"]; +const GEMINI_IMAGE_SIZE_BY_QUALITY: Record = { low: "1K", medium: "2K", high: "4K", standard: "1K", hd: "2K" }; + function normalizeQuality(quality: string) { const value = quality.trim().toLowerCase(); const normalized = QUALITY_ALIASES[value] || value; @@ -180,6 +183,42 @@ function resolveRequestSize(quality: string | undefined, size: string) { throw new Error("图像尺寸格式不支持,请使用 auto、9:16 或 1024x1024"); } +function resolveGeminiImageConfig(config: AiConfig) { + const value = config.size.trim(); + const dimensions = parseImageDimensions(value); + const ratio = dimensions ? `${dimensions.width}:${dimensions.height}` : value; + const aspectRatio = value && value.toLowerCase() !== "auto" ? closestGeminiAspectRatio(ratio) : undefined; + const imageSize = supportsGeminiImageSize(config.model) ? resolveGeminiImageSize(config.quality, dimensions) : undefined; + const image = { ...(aspectRatio ? { aspectRatio } : {}), ...(imageSize ? { imageSize } : {}) }; + return Object.keys(image).length ? { responseFormat: { image } } : {}; +} + +function closestGeminiAspectRatio(value: string) { + const ratio = parseImageRatio(value); + const target = ratio.width / ratio.height; + return GEMINI_SUPPORTED_RATIOS.reduce((best, item) => { + const current = parseImageRatio(item); + const bestRatio = parseImageRatio(best); + return Math.abs(current.width / current.height - target) < Math.abs(bestRatio.width / bestRatio.height - target) ? item : best; + }); +} + +function resolveGeminiImageSize(quality: string, dimensions: { width: number; height: number } | null) { + const normalizedQuality = normalizeQuality(quality); + if (normalizedQuality) return GEMINI_IMAGE_SIZE_BY_QUALITY[normalizedQuality]; + if (!dimensions) return undefined; + const edge = Math.max(dimensions.width, dimensions.height); + if (edge <= 768) return "512"; + if (edge <= 1536) return "1K"; + if (edge <= 3072) return "2K"; + return "4K"; +} + +function supportsGeminiImageSize(model: string) { + const value = model.toLowerCase(); + return value.includes("gemini-3") || value.includes("3.1") || value.includes("3-pro"); +} + function resolveImageDataUrl(item: Record) { if (typeof item.b64_json === "string" && item.b64_json) { return `data:image/png;base64,${item.b64_json}`; @@ -585,7 +624,7 @@ async function requestGeminiImagesOnce(config: AiConfig, prompt: string, referen const response = await axios.post( geminiApiUrl(config, "generateContent"), { - ...toGeminiBody(config, [{ role: "user", content: prompt }], { generationConfig: { responseModalities: ["TEXT", "IMAGE"] } }), + ...toGeminiBody(config, [{ role: "user", content: prompt }], { generationConfig: { responseModalities: ["TEXT", "IMAGE"], ...resolveGeminiImageConfig(config) } }), contents: [{ role: "user", parts }], }, { headers: geminiHeaders(config), signal: options?.signal },