feat(ai): support Qwen Image 3.0
This commit is contained in:
parent
5f6341a318
commit
2a4f8f631b
@ -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;
|
||||
|
||||
@ -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<string, number> = {
|
||||
"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`);
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user