diff --git a/lib/ai/model-classification.ts b/lib/ai/model-classification.ts index eb201ab89..24fee08c2 100644 --- a/lib/ai/model-classification.ts +++ b/lib/ai/model-classification.ts @@ -125,6 +125,11 @@ function normalizeText(value?: string | null) { return String(value || "").trim().toLowerCase(); } +export function isAlibabaImageTextToImageModelId(modelId?: string | null) { + const normalized = normalizeText(modelId); + return /^qwen-image-3(?:\.0)?(?:$|[-/])/.test(normalized); +} + function toRecord(value: unknown) { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; @@ -327,7 +332,7 @@ export function modelSupportsTextToImageDescriptor(params: { } if (providerType === "alibaba-image") { - return false; + return isAlibabaImageTextToImageModelId(modelId); } return true; diff --git a/lib/ai/providers/alibaba-image.ts b/lib/ai/providers/alibaba-image.ts index 9314e4acf..ce1d0e4db 100644 --- a/lib/ai/providers/alibaba-image.ts +++ b/lib/ai/providers/alibaba-image.ts @@ -10,6 +10,7 @@ import sharp from "sharp"; import { constrainImageDimensions } from "@/lib/ai/image-size-constraints"; +import { isAlibabaImageTextToImageModelId } from "@/lib/ai/model-classification"; import { buildReferenceImageOrderPrompt } from "@/lib/ai/providers/reference-image-order"; import { createDeferredUploadedImage } from "@/lib/storage/image-archive"; import { uploadFile } from "@/lib/storage/service"; @@ -535,14 +536,20 @@ async function processResponse( */ function formatImageResponse( images: Array<{ thumbnail: string; original: string }>, - size?: string + size?: string, + requestedSizeLabel?: string, ): string { if (images.length === 0) { return "图片生成失败,请重试。"; } // 将分辨率转换为简短格式(如 "1024*1024" -> "1K") - const sizeLabel = size ? getSizeLabel(size) : "2K"; + const normalizedRequestedSize = normalizeImageSizeLabel(requestedSizeLabel); + const sizeLabel = requestedSizeLabel + ? normalizedRequestedSize + : size + ? getSizeLabel(size) + : "2K"; return images .map((img) => { @@ -663,8 +670,11 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) { preparedImageUrls.push(await prepareAlibabaInputImageUrl(imageUrl, activeModelId)); } - // 验证:qwen-image-edit 系列模型必须有输入图片 - if (preparedImageUrls.length === 0) { + // qwen-image-3.0 同时支持文生图与图生图;旧编辑模型仍必须上传参考图。 + if ( + preparedImageUrls.length === 0 && + !isAlibabaImageTextToImageModelId(activeModelId) + ) { const modelName = options.modelDisplayName || modelId; throw new Error(`「${modelName}」是图片编辑模型,需要至少上传一张图片才能进行编辑。如需纯文生图,请使用其他生图模型。`); } @@ -683,9 +693,10 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) { const imageSize = normalizeImageSizeLabel(options.imageParams?.imageSize); // 根据分辨率等级确定基准像素 + const isQwenImage3 = isAlibabaImageTextToImageModelId(activeModelId); const baseSizeMap: Record = { "1K": 1024, - "2K": 1536, + "2K": isQwenImage3 ? 2048 : 1536, "4K": 2048, }; const baseSize = baseSizeMap[imageSize] || 1536; @@ -725,27 +736,34 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) { // 阿里文档在 auto 下默认接近 1024*1024,这会让 4K 选择失效。 // 这里改为根据参考图比例计算 size,确保 1K/2K/4K 档位真正生效。 const referenceImageUrl = preparedImageUrls[0]; - const inferredDimensions = await inferImageDimensionsFromImageUrl(referenceImageUrl); - if (inferredDimensions) { - params.size = calculateAlibabaSizeFromDimensions( - inferredDimensions.width, - inferredDimensions.height, - baseSize, - ); + if (!referenceImageUrl) { + params.size = `${baseSize}*${baseSize}`; console.log( - `[Alibaba Image] Auto mode resolved size: imageSize=${imageSize}, reference=${inferredDimensions.width}x${inferredDimensions.height}, inferredAspect=${inferredDimensions.aspectRatio} => size=${params.size}` + `[Alibaba Image] Text-to-image auto mode resolved size: imageSize=${imageSize} => size=${params.size}` ); } else { - const inferredAspectRatio = await inferAspectRatioFromImageUrl(referenceImageUrl); - if (inferredAspectRatio) { - params.size = calculateAlibabaSizeByAspect(inferredAspectRatio, baseSize); + const inferredDimensions = await inferImageDimensionsFromImageUrl(referenceImageUrl); + if (inferredDimensions) { + params.size = calculateAlibabaSizeFromDimensions( + inferredDimensions.width, + inferredDimensions.height, + baseSize, + ); console.log( - `[Alibaba Image] Auto mode resolved size via aspect fallback: imageSize=${imageSize}, inferredAspect=${inferredAspectRatio} => size=${params.size}` + `[Alibaba Image] Auto mode resolved size: imageSize=${imageSize}, reference=${inferredDimensions.width}x${inferredDimensions.height}, inferredAspect=${inferredDimensions.aspectRatio} => size=${params.size}` ); } else { - console.log( - `[Alibaba Image] Auto mode keeps provider default size: imageSize=${imageSize}, inferredAspect=unavailable` - ); + const inferredAspectRatio = await inferAspectRatioFromImageUrl(referenceImageUrl); + if (inferredAspectRatio) { + params.size = calculateAlibabaSizeByAspect(inferredAspectRatio, baseSize); + console.log( + `[Alibaba Image] Auto mode resolved size via aspect fallback: imageSize=${imageSize}, inferredAspect=${inferredAspectRatio} => size=${params.size}` + ); + } else { + console.log( + `[Alibaba Image] Auto mode keeps provider default size: imageSize=${imageSize}, inferredAspect=unavailable` + ); + } } } } else { @@ -763,7 +781,11 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) { } // 根据图片数量确定编辑模式 - const editMode = preparedImageUrls.length === 1 ? "单图编辑" : `多图融合 (${preparedImageUrls.length}张)`; + const editMode = preparedImageUrls.length === 0 + ? "文生图" + : preparedImageUrls.length === 1 + ? "单图编辑" + : `多图融合 (${preparedImageUrls.length}张)`; const promptForRequest = buildReferenceImageOrderPrompt( prompt, preparedImageUrls @@ -785,7 +807,7 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) { // 处理响应 const images = await processResponse(response); - const markdownText = formatImageResponse(images, params.size); + const markdownText = formatImageResponse(images, params.size, imageSize); console.log(`[Alibaba Image] Generated ${images.length} images successfully`); diff --git a/tests/routes/model-classification.test.ts b/tests/routes/model-classification.test.ts index 2c5297a50..624729fc4 100644 --- a/tests/routes/model-classification.test.ts +++ b/tests/routes/model-classification.test.ts @@ -13,6 +13,8 @@ import { is3DModelDescriptor, isImageGenerationModelDescriptor, isVideoModelDescriptor, + isAlibabaImageTextToImageModelId, + modelSupportsTextToImageDescriptor, } from "@/lib/ai/model-classification"; import { buildVip98kModelPresets, @@ -29,6 +31,25 @@ const atlasHunyuan3D = { }; test.describe("AI model media classification", () => { + test("classifies Qwen Image 3.0 as Alibaba text-to-image and image editing", () => { + expect(isAlibabaImageTextToImageModelId("qwen-image-3.0")).toBe(true); + expect(isAlibabaImageTextToImageModelId("qwen-image-edit-plus")).toBe(false); + expect( + modelSupportsTextToImageDescriptor({ + providerType: "alibaba-image", + modelId: "qwen-image-3.0", + supportsImageGen: true, + }) + ).toBe(true); + expect( + modelSupportsTextToImageDescriptor({ + providerType: "alibaba-image", + modelId: "qwen-image-edit-plus", + supportsImageGen: true, + }) + ).toBe(false); + }); + test("preserves administrator-defined AtlasCloud display names", () => { expect( resolveAtlasCloudDisplayName(