528 lines
16 KiB
TypeScript
528 lines
16 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
createAtlasCloudImagePreset,
|
|
inferAtlasCloudMediaKind,
|
|
resolveAtlasCloudDisplayName,
|
|
shouldInsertMissingAtlasCloudPreset,
|
|
} from "@/lib/ai/atlascloud-models";
|
|
import {
|
|
createAtlasCloudRequestBody,
|
|
createAtlasCloudVideoRequestBody,
|
|
} from "@/lib/ai/atlascloud-request";
|
|
import {
|
|
is3DModelDescriptor,
|
|
isImageGenerationModelDescriptor,
|
|
isVideoModelDescriptor,
|
|
isAlibabaImageTextToImageModelId,
|
|
modelSupportsTextToImageDescriptor,
|
|
} from "@/lib/ai/model-classification";
|
|
import {
|
|
buildVip98kModelPresets,
|
|
isVip98kCatalogModelEligible,
|
|
isVip98kImageModelVisible,
|
|
} from "@/lib/ai/vip98k-models";
|
|
|
|
const atlasHunyuan3D = {
|
|
model: "tencent/hunyuan3d-pro/text-to-3d",
|
|
type: "Image",
|
|
displayName: "Hunyuan 3D Pro Text-to-3D",
|
|
categories: ["TEXT-TO-3D"],
|
|
tags: ["TEXT-TO-3D", "NEW"],
|
|
};
|
|
|
|
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(
|
|
"视觉 Flux Kontext Pro",
|
|
"Flux Kontext Pro",
|
|
"black-forest-labs/flux-kontext-pro"
|
|
)
|
|
).toBe("视觉 Flux Kontext Pro");
|
|
expect(
|
|
resolveAtlasCloudDisplayName(
|
|
"",
|
|
"Flux Kontext Pro",
|
|
"black-forest-labs/flux-kontext-pro"
|
|
)
|
|
).toBe("Flux Kontext Pro");
|
|
});
|
|
|
|
test("adds a missing Atlas media kind without restoring deleted models in an existing kind", () => {
|
|
const existingKinds = new Set(["image", "3d"]);
|
|
expect(shouldInsertMissingAtlasCloudPreset(existingKinds, "video")).toBe(true);
|
|
expect(shouldInsertMissingAtlasCloudPreset(existingKinds, "image")).toBe(false);
|
|
expect(shouldInsertMissingAtlasCloudPreset(existingKinds, "3d")).toBe(false);
|
|
});
|
|
|
|
test("keeps multi-reference BananaImage models classified as image models", () => {
|
|
const descriptor = {
|
|
providerType: "bananaimage",
|
|
modelId: "gemini-3-pro-image-preview",
|
|
capabilities: {
|
|
supportedModes: [
|
|
"text-to-image",
|
|
"image-to-image",
|
|
"reference-images",
|
|
],
|
|
},
|
|
supportsImageGen: true,
|
|
};
|
|
|
|
expect(isVideoModelDescriptor(descriptor)).toBe(false);
|
|
expect(isImageGenerationModelDescriptor(descriptor)).toBe(true);
|
|
});
|
|
|
|
test("still recognizes explicit video generation modes", () => {
|
|
expect(
|
|
isVideoModelDescriptor({
|
|
providerType: "custom",
|
|
modelId: "custom-video-model",
|
|
capabilities: { supportedModes: ["text-to-video"] },
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
test("keeps Atlas 3D entries out of image and video categories", () => {
|
|
expect(inferAtlasCloudMediaKind(atlasHunyuan3D)).toBe("3d");
|
|
|
|
const descriptor = {
|
|
providerType: "atlascloud",
|
|
modelId: atlasHunyuan3D.model,
|
|
capabilities: {
|
|
kind: "3d",
|
|
mediaType: "3d",
|
|
supportedModes: ["text-to-3d"],
|
|
},
|
|
supportsImageGen: false,
|
|
};
|
|
|
|
expect(is3DModelDescriptor(descriptor)).toBe(true);
|
|
expect(isImageGenerationModelDescriptor(descriptor)).toBe(false);
|
|
expect(isVideoModelDescriptor(descriptor)).toBe(false);
|
|
});
|
|
|
|
test("writes 3D Atlas presets with model-level capabilities", () => {
|
|
const preset = createAtlasCloudImagePreset(
|
|
atlasHunyuan3D,
|
|
{
|
|
components: {
|
|
schemas: {
|
|
Input: {
|
|
type: "object",
|
|
properties: {
|
|
model: { type: "string" },
|
|
prompt: { type: "string" },
|
|
},
|
|
required: ["model", "prompt"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
0
|
|
);
|
|
|
|
expect(preset.supportsImageGen).toBe(false);
|
|
expect(preset.capabilities).toMatchObject({
|
|
kind: "3d",
|
|
mediaType: "3d",
|
|
supportedModes: ["text-to-3d"],
|
|
});
|
|
});
|
|
|
|
test("still distinguishes Atlas image and video descriptors", () => {
|
|
const imageDescriptor = {
|
|
providerType: "atlascloud",
|
|
modelId: "black-forest-labs/flux-2-pro/text-to-image",
|
|
capabilities: {
|
|
kind: "text-to-image",
|
|
mediaType: "image",
|
|
supportedModes: ["text-to-image"],
|
|
},
|
|
supportsImageGen: true,
|
|
};
|
|
const videoDescriptor = {
|
|
providerType: "atlascloud",
|
|
modelId: "veed/fabric-1.0/image-to-video",
|
|
capabilities: {
|
|
kind: "video",
|
|
mediaType: "video",
|
|
supportedModes: ["image-to-video"],
|
|
},
|
|
supportsImageGen: false,
|
|
};
|
|
|
|
expect(isImageGenerationModelDescriptor(imageDescriptor)).toBe(true);
|
|
expect(isVideoModelDescriptor(imageDescriptor)).toBe(false);
|
|
expect(isVideoModelDescriptor(videoDescriptor)).toBe(true);
|
|
expect(isImageGenerationModelDescriptor(videoDescriptor)).toBe(false);
|
|
});
|
|
|
|
test("maps Atlas reference-video image models to a valid frontend request", () => {
|
|
const preset = createAtlasCloudImagePreset(
|
|
{
|
|
model: "google/nano-banana-2/reference-to-image",
|
|
type: "Image",
|
|
displayName: "Nano Banana 2 Reference to Image",
|
|
categories: ["IMAGE-TO-IMAGE"],
|
|
},
|
|
{
|
|
paths: {
|
|
"/api/v1/model/generateImage": { post: {} },
|
|
"/api/v1/model/prediction/{request_id}": { get: {} },
|
|
},
|
|
components: {
|
|
schemas: {
|
|
Input: {
|
|
type: "object",
|
|
properties: {
|
|
model: { type: "string" },
|
|
prompt: { type: "string" },
|
|
resolution: { type: "string", enum: ["1k", "2k", "4k"] },
|
|
video_clips: {
|
|
type: "array",
|
|
minItems: 1,
|
|
maxItems: 1,
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
url: { type: "string" },
|
|
start: { type: "number", default: 0 },
|
|
ends: { type: "number", default: 0 },
|
|
fps: { type: "number", default: 1 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
required: ["model", "prompt", "video_clips"],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
expect(preset.capabilities).toMatchObject({
|
|
mediaType: "image",
|
|
supportsVideoReference: true,
|
|
requiresVideoReference: true,
|
|
maxVideoReferences: 1,
|
|
});
|
|
const { body } = createAtlasCloudRequestBody({
|
|
modelId: preset.modelId,
|
|
prompt: "参考视频生成商品图",
|
|
referenceImages: [],
|
|
referenceVideos: ["https://example.com/reference.mp4"],
|
|
imageParams: { imageSize: "2K", aspectRatio: "1:1" },
|
|
modelConfig: preset.imageGenConfig,
|
|
});
|
|
expect(body.video_clips).toEqual([
|
|
{
|
|
url: "https://example.com/reference.mp4",
|
|
start: 0,
|
|
ends: 0,
|
|
fps: 1,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("maps Atlas video schema capabilities and frontend parameters", () => {
|
|
const preset = createAtlasCloudImagePreset(
|
|
{
|
|
model: "bytedance/seedance-2.0-mini/image-to-video",
|
|
type: "Video",
|
|
displayName: "Seedance 2.0 Mini Image to Video",
|
|
categories: ["IMAGE-TO-VIDEO"],
|
|
},
|
|
{
|
|
paths: {
|
|
"/api/v1/model/generateVideo": { post: {} },
|
|
"/api/v1/model/prediction/{request_id}": { get: {} },
|
|
},
|
|
components: {
|
|
schemas: {
|
|
Input: {
|
|
type: "object",
|
|
properties: {
|
|
model: { type: "string" },
|
|
prompt: { type: "string" },
|
|
image: { type: "string", "x-format": "image" },
|
|
last_image: { type: "string", "x-format": "image" },
|
|
resolution: {
|
|
type: "string",
|
|
enum: ["480p", "720p", "1080p"],
|
|
default: "720p",
|
|
},
|
|
ratio: {
|
|
type: "string",
|
|
enum: ["16:9", "9:16", "adaptive"],
|
|
default: "adaptive",
|
|
},
|
|
duration: {
|
|
type: "integer",
|
|
enum: [5, 10],
|
|
default: 5,
|
|
},
|
|
seed: { type: "integer" },
|
|
watermark: { type: "boolean", default: false },
|
|
},
|
|
required: ["model", "prompt", "image"],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
expect(preset.supportsImageGen).toBe(false);
|
|
expect(preset.capabilities).toMatchObject({
|
|
kind: "video",
|
|
mediaType: "video",
|
|
supportedModes: ["first-frame", "first-last-frame"],
|
|
supportedResolutions: ["480p", "720p", "1080p"],
|
|
supportedRatios: ["16:9", "9:16", "adaptive"],
|
|
durationOptions: [5, 10],
|
|
});
|
|
|
|
const { body } = createAtlasCloudVideoRequestBody({
|
|
modelId: preset.modelId,
|
|
options: {
|
|
model: preset.modelId,
|
|
prompt: "让商品缓慢旋转",
|
|
mode: "first-last-frame",
|
|
firstFrameImage: "https://example.com/first.png",
|
|
lastFrameImage: "https://example.com/last.png",
|
|
resolution: "1080p",
|
|
ratio: "9:16",
|
|
duration: 10,
|
|
watermark: true,
|
|
},
|
|
referenceImages: [
|
|
"https://example.com/first.png",
|
|
"https://example.com/last.png",
|
|
],
|
|
modelConfig: preset.imageGenConfig,
|
|
});
|
|
expect(body).toMatchObject({
|
|
model: preset.modelId,
|
|
prompt: "让商品缓慢旋转",
|
|
image: "https://example.com/first.png",
|
|
last_image: "https://example.com/last.png",
|
|
resolution: "1080p",
|
|
ratio: "9:16",
|
|
duration: 10,
|
|
watermark: true,
|
|
});
|
|
});
|
|
test("normalizes VIP98K media and tool types before they reach the frontend", () => {
|
|
const presets = buildVip98kModelPresets([
|
|
{
|
|
modelId: "mislabelled-video",
|
|
displayName: "Mislabelled video",
|
|
type: "image",
|
|
capabilities: ["text_to_video", "image_to_video"],
|
|
inputModes: ["text", "image"],
|
|
},
|
|
{
|
|
modelId: "enhance",
|
|
displayName: "Enhance",
|
|
type: "image",
|
|
capabilities: ["image_processing"],
|
|
},
|
|
{
|
|
modelId: "image-expansion",
|
|
displayName: "Extend",
|
|
type: "image_tool",
|
|
capabilities: ["outpainting"],
|
|
},
|
|
{
|
|
modelId: "remove-background",
|
|
displayName: "Remove background",
|
|
type: "image_tool",
|
|
capabilities: ["image_processing"],
|
|
},
|
|
{
|
|
modelId: "multimodal-model",
|
|
displayName: "Multimodal model",
|
|
type: "model",
|
|
capabilities: ["multimodal"],
|
|
inputModes: ["text", "image"],
|
|
},
|
|
{
|
|
modelId: "tencent/hunyuan3d-pro/text-to-3d",
|
|
displayName: "Hunyuan 3D",
|
|
type: "image",
|
|
capabilities: ["text-to-3d"],
|
|
},
|
|
]);
|
|
|
|
expect(presets.find((item) => item.modelId === "mislabelled-video")).toMatchObject({
|
|
type: "video",
|
|
supportsImageGen: false,
|
|
});
|
|
expect(presets.find((item) => item.modelId === "enhance")).toMatchObject({
|
|
type: "image_tool",
|
|
isHidden: true,
|
|
capabilities: { toolOperation: "enhance" },
|
|
});
|
|
expect(presets.find((item) => item.modelId === "image-expansion")).toMatchObject({
|
|
type: "image_tool",
|
|
capabilities: { toolOperation: "extend" },
|
|
});
|
|
expect(presets.find((item) => item.modelId === "remove-background")).toMatchObject({
|
|
type: "image_tool",
|
|
capabilities: { toolOperation: "remove-background" },
|
|
});
|
|
expect(presets.find((item) => item.modelId === "multimodal-model")).toMatchObject({
|
|
type: "model",
|
|
supportsVision: true,
|
|
supportsImageGen: false,
|
|
});
|
|
expect(
|
|
presets.find((item) => item.modelId === "tencent/hunyuan3d-pro/text-to-3d"),
|
|
).toMatchObject({
|
|
type: "model_3d",
|
|
supportsImageGen: false,
|
|
isHidden: true,
|
|
capabilities: { kind: "model-3d" },
|
|
});
|
|
});
|
|
|
|
test("only shows VIP98K image models verified as released in 2026 or later", () => {
|
|
const models = [
|
|
{
|
|
modelId: "black-forest-labs/flux-dev",
|
|
displayName: "Flux Dev",
|
|
type: "image",
|
|
capabilities: ["image_to_image"],
|
|
},
|
|
{
|
|
modelId: "google/nano-banana-2/edit",
|
|
displayName: "Nano Banana 2",
|
|
type: "image",
|
|
capabilities: ["image_to_image"],
|
|
},
|
|
{
|
|
modelId: "future-image-with-date",
|
|
displayName: "Future image",
|
|
type: "image",
|
|
capabilities: ["image_to_image"],
|
|
releasedAt: "2027-02-01",
|
|
},
|
|
{
|
|
modelId: "old-image-with-date",
|
|
displayName: "Old image",
|
|
type: "image",
|
|
capabilities: ["image_to_image"],
|
|
releasedAt: "2025-12-15",
|
|
},
|
|
];
|
|
|
|
expect(isVip98kImageModelVisible(models[0])).toBe(false);
|
|
expect(isVip98kImageModelVisible(models[1])).toBe(true);
|
|
expect(isVip98kImageModelVisible(models[2])).toBe(true);
|
|
expect(isVip98kImageModelVisible(models[3])).toBe(false);
|
|
});
|
|
|
|
test("excludes pure text image/video models but keeps reference and 3D models", () => {
|
|
expect(
|
|
isVip98kCatalogModelEligible({
|
|
modelId: "openai/gpt-image-2/text-to-image",
|
|
type: "image",
|
|
capabilities: ["text-to-image"],
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isVip98kCatalogModelEligible({
|
|
modelId: "google/veo/text-to-video",
|
|
type: "video",
|
|
capabilities: ["text-to-video"],
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isVip98kCatalogModelEligible({
|
|
modelId: "google/nano-banana-2/edit",
|
|
type: "image",
|
|
capabilities: ["text-to-image", "image-to-image"],
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isVip98kCatalogModelEligible({
|
|
modelId: "bytedance/seedance/image-to-video",
|
|
type: "video",
|
|
capabilities: ["text-to-video", "image-to-video"],
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isVip98kCatalogModelEligible({
|
|
modelId: "tencent/hunyuan3d-pro/text-to-3d",
|
|
type: "image",
|
|
capabilities: ["text-to-3d"],
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("excludes catalog models explicitly marked unavailable", () => {
|
|
expect(
|
|
isVip98kCatalogModelEligible({
|
|
modelId: "google/nano-banana-2/edit",
|
|
type: "image",
|
|
capabilities: ["image-to-image"],
|
|
status: "closed",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("excludes VIP98K text-to-image models without applying a price limit", () => {
|
|
const presets = buildVip98kModelPresets([
|
|
{
|
|
modelId: "pure-text-to-image",
|
|
type: "image",
|
|
capabilities: ["text_to_image"],
|
|
pricing: { effective_price: 0.02 },
|
|
},
|
|
{
|
|
modelId: "mixed-image-model",
|
|
type: "image",
|
|
capabilities: ["text_to_image", "image_to_image"],
|
|
pricing: { effective_price: 0.2 },
|
|
},
|
|
{
|
|
modelId: "expensive-image-edit",
|
|
type: "image",
|
|
capabilities: ["image_to_image"],
|
|
pricing: { effective_price: 2.5 },
|
|
},
|
|
{
|
|
modelId: "remove-background",
|
|
type: "image_tool",
|
|
capabilities: ["image_processing"],
|
|
pricing: { effective_price: 1.2 },
|
|
},
|
|
]);
|
|
|
|
expect(presets.map((item) => item.modelId)).toEqual([
|
|
"mixed-image-model",
|
|
"expensive-image-edit",
|
|
"remove-background",
|
|
]);
|
|
});
|
|
});
|