From 9623f5365ba97870ecafd0200f66f2e5ed1cad46 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 20 Feb 2026 17:12:00 +0800 Subject: [PATCH] fix: enforce imageSize in alibaba auto aspect mode --- lib/ai/providers/alibaba-image.ts | 174 +++++++++++++++++++----------- 1 file changed, 112 insertions(+), 62 deletions(-) diff --git a/lib/ai/providers/alibaba-image.ts b/lib/ai/providers/alibaba-image.ts index 60e88cfcf..f1e2552c9 100644 --- a/lib/ai/providers/alibaba-image.ts +++ b/lib/ai/providers/alibaba-image.ts @@ -47,6 +47,79 @@ interface AlibabaImageResponse { message?: string; } +const ALIBABA_IMAGE_FETCH_TIMEOUT_MS = 20000; +const ALIBABA_MIN_SIZE = 512; +const ALIBABA_MAX_SIZE = 2048; + +function normalizeImageSizeLabel(imageSize?: string): "1K" | "2K" | "4K" { + const normalized = String(imageSize || "").trim().toUpperCase(); + if (normalized === "1K" || normalized === "2K" || normalized === "4K") return normalized; + return "2K"; +} + +function calculateAlibabaSizeByAspect(aspectValue: number, baseSize: number): string { + const safeAspect = Number.isFinite(aspectValue) && aspectValue > 0 ? aspectValue : 1; + const totalPixels = baseSize * baseSize; + + let height = Math.round(Math.sqrt(totalPixels / safeAspect)); + let width = Math.round(height * safeAspect); + + if (width > ALIBABA_MAX_SIZE) { + const scale = ALIBABA_MAX_SIZE / width; + width = ALIBABA_MAX_SIZE; + height = Math.round(height * scale); + } + if (height > ALIBABA_MAX_SIZE) { + const scale = ALIBABA_MAX_SIZE / height; + height = ALIBABA_MAX_SIZE; + width = Math.round(width * scale); + } + if (width < ALIBABA_MIN_SIZE) { + const scale = ALIBABA_MIN_SIZE / width; + width = ALIBABA_MIN_SIZE; + height = Math.round(height * scale); + } + if (height < ALIBABA_MIN_SIZE) { + const scale = ALIBABA_MIN_SIZE / height; + height = ALIBABA_MIN_SIZE; + width = Math.round(width * scale); + } + + width = Math.max(ALIBABA_MIN_SIZE, Math.min(ALIBABA_MAX_SIZE, width)); + height = Math.max(ALIBABA_MIN_SIZE, Math.min(ALIBABA_MAX_SIZE, height)); + + width = Math.round(width / 2) * 2; + height = Math.round(height / 2) * 2; + + return `${width}*${height}`; +} + +async function inferAspectRatioFromImageUrl(imageUrl: string): Promise { + if (!/^https?:\/\//i.test(imageUrl)) return null; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), ALIBABA_IMAGE_FETCH_TIMEOUT_MS); + try { + const response = await fetch(imageUrl, { signal: controller.signal }); + if (!response.ok) { + console.warn("[Alibaba Image] Failed to fetch reference image for auto size:", response.status); + return null; + } + + const buffer = Buffer.from(await response.arrayBuffer()); + if (!buffer.length) return null; + + const metadata = await sharp(buffer).metadata(); + if (!metadata.width || !metadata.height || metadata.height <= 0) return null; + return metadata.width / metadata.height; + } catch (error) { + console.warn("[Alibaba Image] Failed to infer aspect ratio from reference image:", error); + return null; + } finally { + clearTimeout(timeoutId); + } +} + /** * 创建缩略图 */ @@ -272,7 +345,19 @@ function getSizeLabel(size: string): string { "1536*1536": "2K", "2048*2048": "4K", }; - return sizeMap[size] || "2K"; + if (sizeMap[size]) return sizeMap[size]; + + const matched = size.match(/^(\d+)\*(\d+)$/); + if (matched) { + const width = Number.parseInt(matched[1], 10); + const height = Number.parseInt(matched[2], 10); + const maxSide = Math.max(width, height); + if (maxSide >= 1900) return "4K"; + if (maxSide >= 1450) return "2K"; + if (maxSide >= 950) return "1K"; + } + + return "2K"; } /** @@ -372,7 +457,7 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) { // 处理分辨率:结合 aspectRatio(宽高比)和 imageSize(分辨率等级) const aspectRatio = options.imageParams?.aspectRatio || "auto"; - const imageSize = options.imageParams?.imageSize || "2K"; + const imageSize = normalizeImageSizeLabel(options.imageParams?.imageSize); // 根据分辨率等级确定基准像素 const baseSizeMap: Record = { @@ -385,12 +470,6 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) { // 根据宽高比计算实际分辨率 // 阿里云 API 要求宽高在 512-2048 之间 const calculateSize = (ratio: string, base: number): string | null => { - // 如果是 auto 模式,返回 null,让 API 根据输入图片自动决定 - // 根据阿里云文档:默认(不指定时):总像素接近 1024*1024,宽高比与输入图(多图输入时为最后一张)一致 - if (ratio === "auto") { - return null; - } - // 解析宽高比 const ratioMap: Record = { "1:1": [1, 1], @@ -405,70 +484,42 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) { "21:9": [21, 9], }; - const [w, h] = ratioMap[ratio] || [1, 1]; + if (!ratioMap[ratio]) return null; + const [w, h] = ratioMap[ratio]; const aspectValue = w / h; - - // 计算宽高,保持总像素接近 base*base - const totalPixels = base * base; - - // height = sqrt(totalPixels / aspectValue) - // width = height * aspectValue - let height = Math.round(Math.sqrt(totalPixels / aspectValue)); - let width = Math.round(height * aspectValue); - - // 阿里云 API 限制:宽高在 512-2048 之间 - const MIN_SIZE = 512; - const MAX_SIZE = 2048; - - // 如果超出范围,按比例缩放,同时保持宽高比 - if (width > MAX_SIZE) { - const scale = MAX_SIZE / width; - width = MAX_SIZE; - height = Math.round(height * scale); - } - if (height > MAX_SIZE) { - const scale = MAX_SIZE / height; - height = MAX_SIZE; - width = Math.round(width * scale); - } - if (width < MIN_SIZE) { - const scale = MIN_SIZE / width; - width = MIN_SIZE; - height = Math.round(height * scale); - } - if (height < MIN_SIZE) { - const scale = MIN_SIZE / height; - height = MIN_SIZE; - width = Math.round(width * scale); - } - - // 最终 clamp 确保在范围内 - width = Math.max(MIN_SIZE, Math.min(MAX_SIZE, width)); - height = Math.max(MIN_SIZE, Math.min(MAX_SIZE, height)); - - // 确保是偶数(某些图像处理可能需要) - width = Math.round(width / 2) * 2; - height = Math.round(height / 2) * 2; - - return `${width}*${height}`; + return calculateAlibabaSizeByAspect(aspectValue, base); }; // 处理 size 参数 // 1. 如果用户直接指定了 size,使用用户指定的 - // 2. 如果是 auto 模式,不传 size,让 API 根据输入图片自动决定比例 + // 2. 如果是 auto 模式,按参考图宽高比计算 size,确保清晰度档位生效 // 3. 如果指定了具体比例,根据 aspectRatio 和 imageSize 计算 if (options.imageParams?.size) { params.size = options.imageParams.size; console.log(`[Alibaba Image] Using user-specified size: ${params.size}`); } else { - const calculatedSize = calculateSize(aspectRatio, baseSize); - if (calculatedSize) { - params.size = calculatedSize; - console.log(`[Alibaba Image] Calculated size: aspectRatio=${aspectRatio}, imageSize=${imageSize} => size=${params.size}`); + if (aspectRatio === "auto") { + // 阿里文档在 auto 下默认接近 1024*1024,这会让 4K 选择失效。 + // 这里改为根据参考图比例计算 size,确保 1K/2K/4K 档位真正生效。 + const referenceImageUrl = imageUrls[imageUrls.length - 1]; + const inferredAspectRatio = await inferAspectRatioFromImageUrl(referenceImageUrl); + params.size = inferredAspectRatio + ? calculateAlibabaSizeByAspect(inferredAspectRatio, baseSize) + : `${baseSize}*${baseSize}`; + console.log( + `[Alibaba Image] Auto mode resolved size: imageSize=${imageSize}, inferredAspect=${inferredAspectRatio || "fallback-square"} => size=${params.size}` + ); } else { - // auto 模式:不设置 size,让 API 根据输入图片自动决定 - // 阿里云会保持输入图片的宽高比,总像素接近 1024*1024 - console.log(`[Alibaba Image] Auto mode: not setting size, API will use input image's aspect ratio`); + const calculatedSize = calculateSize(aspectRatio, baseSize); + if (calculatedSize) { + params.size = calculatedSize; + console.log(`[Alibaba Image] Calculated size: aspectRatio=${aspectRatio}, imageSize=${imageSize} => size=${params.size}`); + } else { + params.size = `${baseSize}*${baseSize}`; + console.log( + `[Alibaba Image] Unknown aspect ratio "${aspectRatio}", fallback size=${params.size}` + ); + } } } @@ -536,4 +587,3 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) { } export default createAlibabaImage; -