feat(ai): support Qwen Image 3.0
This commit is contained in:
parent
d005b3477a
commit
ba232f3385
@ -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;
|
||||||
|
|||||||
@ -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`);
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user