feat(ai): support Qwen Image 3.0

This commit is contained in:
134 2026-08-26 16:10:00 +08:00
parent d005b3477a
commit ba232f3385
3 changed files with 71 additions and 23 deletions

View File

@ -125,6 +125,11 @@ function normalizeText(value?: string | null) {
return String(value || "").trim().toLowerCase(); 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) { function toRecord(value: unknown) {
if (!value || typeof value !== "object" || Array.isArray(value)) { if (!value || typeof value !== "object" || Array.isArray(value)) {
return null; return null;
@ -327,7 +332,7 @@ export function modelSupportsTextToImageDescriptor(params: {
} }
if (providerType === "alibaba-image") { if (providerType === "alibaba-image") {
return false; return isAlibabaImageTextToImageModelId(modelId);
} }
return true; return true;

View File

@ -10,6 +10,7 @@
import sharp from "sharp"; import sharp from "sharp";
import { constrainImageDimensions } from "@/lib/ai/image-size-constraints"; 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 { buildReferenceImageOrderPrompt } from "@/lib/ai/providers/reference-image-order";
import { createDeferredUploadedImage } from "@/lib/storage/image-archive"; import { createDeferredUploadedImage } from "@/lib/storage/image-archive";
import { uploadFile } from "@/lib/storage/service"; import { uploadFile } from "@/lib/storage/service";
@ -535,14 +536,20 @@ async function processResponse(
*/ */
function formatImageResponse( function formatImageResponse(
images: Array<{ thumbnail: string; original: string }>, images: Array<{ thumbnail: string; original: string }>,
size?: string size?: string,
requestedSizeLabel?: string,
): string { ): string {
if (images.length === 0) { if (images.length === 0) {
return "图片生成失败,请重试。"; return "图片生成失败,请重试。";
} }
// 将分辨率转换为简短格式(如 "1024*1024" -> "1K") // 将分辨率转换为简短格式(如 "1024*1024" -> "1K")
const sizeLabel = size ? getSizeLabel(size) : "2K"; const normalizedRequestedSize = normalizeImageSizeLabel(requestedSizeLabel);
const sizeLabel = requestedSizeLabel
? normalizedRequestedSize
: size
? getSizeLabel(size)
: "2K";
return images return images
.map((img) => { .map((img) => {
@ -663,8 +670,11 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
preparedImageUrls.push(await prepareAlibabaInputImageUrl(imageUrl, activeModelId)); preparedImageUrls.push(await prepareAlibabaInputImageUrl(imageUrl, activeModelId));
} }
// 验证:qwen-image-edit 系列模型必须有输入图片 // qwen-image-3.0 同时支持文生图与图生图;旧编辑模型仍必须上传参考图。
if (preparedImageUrls.length === 0) { if (
preparedImageUrls.length === 0 &&
!isAlibabaImageTextToImageModelId(activeModelId)
) {
const modelName = options.modelDisplayName || modelId; const modelName = options.modelDisplayName || modelId;
throw new Error(`「${modelName}」是图片编辑模型,需要至少上传一张图片才能进行编辑。如需纯文生图,请使用其他生图模型。`); throw new Error(`「${modelName}」是图片编辑模型,需要至少上传一张图片才能进行编辑。如需纯文生图,请使用其他生图模型。`);
} }
@ -683,9 +693,10 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
const imageSize = normalizeImageSizeLabel(options.imageParams?.imageSize); const imageSize = normalizeImageSizeLabel(options.imageParams?.imageSize);
// 根据分辨率等级确定基准像素 // 根据分辨率等级确定基准像素
const isQwenImage3 = isAlibabaImageTextToImageModelId(activeModelId);
const baseSizeMap: Record<string, number> = { const baseSizeMap: Record<string, number> = {
"1K": 1024, "1K": 1024,
"2K": 1536, "2K": isQwenImage3 ? 2048 : 1536,
"4K": 2048, "4K": 2048,
}; };
const baseSize = baseSizeMap[imageSize] || 1536; const baseSize = baseSizeMap[imageSize] || 1536;
@ -725,27 +736,34 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
// 阿里文档在 auto 下默认接近 1024*1024,这会让 4K 选择失效。 // 阿里文档在 auto 下默认接近 1024*1024,这会让 4K 选择失效。
// 这里改为根据参考图比例计算 size,确保 1K/2K/4K 档位真正生效。 // 这里改为根据参考图比例计算 size,确保 1K/2K/4K 档位真正生效。
const referenceImageUrl = preparedImageUrls[0]; const referenceImageUrl = preparedImageUrls[0];
const inferredDimensions = await inferImageDimensionsFromImageUrl(referenceImageUrl); if (!referenceImageUrl) {
if (inferredDimensions) { params.size = `${baseSize}*${baseSize}`;
params.size = calculateAlibabaSizeFromDimensions(
inferredDimensions.width,
inferredDimensions.height,
baseSize,
);
console.log( 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 { } else {
const inferredAspectRatio = await inferAspectRatioFromImageUrl(referenceImageUrl); const inferredDimensions = await inferImageDimensionsFromImageUrl(referenceImageUrl);
if (inferredAspectRatio) { if (inferredDimensions) {
params.size = calculateAlibabaSizeByAspect(inferredAspectRatio, baseSize); params.size = calculateAlibabaSizeFromDimensions(
inferredDimensions.width,
inferredDimensions.height,
baseSize,
);
console.log( 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 { } else {
console.log( const inferredAspectRatio = await inferAspectRatioFromImageUrl(referenceImageUrl);
`[Alibaba Image] Auto mode keeps provider default size: imageSize=${imageSize}, inferredAspect=unavailable` 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 { } 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( const promptForRequest = buildReferenceImageOrderPrompt(
prompt, prompt,
preparedImageUrls preparedImageUrls
@ -785,7 +807,7 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
// 处理响应 // 处理响应
const images = await processResponse(response); 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`); console.log(`[Alibaba Image] Generated ${images.length} images successfully`);

View File

@ -13,6 +13,8 @@ import {
is3DModelDescriptor, is3DModelDescriptor,
isImageGenerationModelDescriptor, isImageGenerationModelDescriptor,
isVideoModelDescriptor, isVideoModelDescriptor,
isAlibabaImageTextToImageModelId,
modelSupportsTextToImageDescriptor,
} from "@/lib/ai/model-classification"; } from "@/lib/ai/model-classification";
import { import {
buildVip98kModelPresets, buildVip98kModelPresets,
@ -29,6 +31,25 @@ const atlasHunyuan3D = {
}; };
test.describe("AI model media classification", () => { 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", () => { test("preserves administrator-defined AtlasCloud display names", () => {
expect( expect(
resolveAtlasCloudDisplayName( resolveAtlasCloudDisplayName(