mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
💄 style(image,video): extend more AIGC params support (#13597)
* 🐛 fix(image,video): preserve prompt and image when switching model * ✨ feat(image): smart imageUrl ↔ imageUrls conversion on model switch - When switching from multi-image to single-image model: use imageUrls[0] as imageUrl - When switching from single-image to multi-image model: wrap imageUrl into [imageUrl] as imageUrls - Preserves prompt and other compatible parameters - Add test cases for bidirectional conversion ♻️ refactor(image): simplify preserveImageInputParams logic - Remove intermediate variables for cleaner code readability - Condense 9 intermediate variables to 3 core ones - Inline condition checks for simpler if statements - Improve code clarity without changing functionality * 🐛 fix(image): preserve imageUrl when target imageUrls default is empty array * chore: format imageUrl & imageUrls * feat: support imageUrls for videoGen fix: fix ci error fix: fix ci error fix: fix + button fix: fix batch images display fix: fix muti images upload display fix: fix ci error style: add Seedance 2.0 support style: add Seedance 2.0 support fix: fix veo imageUrls logic * style: add watermark & prompt_extend & web_search support style: update minimax & seedream price style: fix fix ui error style: update z-image style: fix video ui style: fix seedance & seedream params style: fix seedance & seedream params style: fix seedance & seedream params fix ci error Update createImage.ts fix ci error fix ci error fix ci error fix ci error fix ci error fix ci error fix: fix optimize_prompt_options * fix rebase issue * fix: seedance 2.0 price missing * fix: apply some suggestions
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"config.model.label": "Model",
|
||||
"config.prompt.placeholder": "Describe what you want to generate",
|
||||
"config.prompt.placeholderWithRef": "Describe how you want to adjust the image",
|
||||
"config.promptExtend.label": "Extended Prompt",
|
||||
"config.quality.label": "Image Quality",
|
||||
"config.quality.options.hd": "High Definition",
|
||||
"config.quality.options.standard": "Standard",
|
||||
@@ -24,6 +25,8 @@
|
||||
"config.size.label": "Size",
|
||||
"config.steps.label": "Steps",
|
||||
"config.title": "Configuration",
|
||||
"config.watermark.label": "Watermark",
|
||||
"config.webSearch.label": "Web Search",
|
||||
"config.width.label": "Width",
|
||||
"generation.actions.applySeed": "Apply Seed",
|
||||
"generation.actions.copyError": "Copy Error Message",
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
"config.imageUrl.label": "Start Frame",
|
||||
"config.prompt.placeholder": "Describe the video you want to generate",
|
||||
"config.prompt.placeholderWithRef": "Describe the scene you want to generate with the image",
|
||||
"config.promptExtend.label": "Prompt Extend",
|
||||
"config.referenceImage.label": "Reference Image",
|
||||
"config.resolution.label": "Resolution",
|
||||
"config.seed.label": "Seed",
|
||||
"config.seed.random": "Random",
|
||||
"config.size.label": "Size",
|
||||
"config.watermark.label": "Watermark",
|
||||
"config.webSearch.label": "Web Search",
|
||||
"generation.actions.copyError": "Copy Error Message",
|
||||
"generation.actions.errorCopied": "Error Message Copied to Clipboard",
|
||||
"generation.actions.errorCopyFailed": "Failed to Copy Error Message",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"config.model.label": "模型",
|
||||
"config.prompt.placeholder": "描述你想要生成的内容",
|
||||
"config.prompt.placeholderWithRef": "描述你想如何调整图片",
|
||||
"config.promptExtend.label": "提示词扩展",
|
||||
"config.quality.label": "图片质量",
|
||||
"config.quality.options.hd": "高清",
|
||||
"config.quality.options.standard": "标准",
|
||||
@@ -24,6 +25,8 @@
|
||||
"config.size.label": "尺寸",
|
||||
"config.steps.label": "步数",
|
||||
"config.title": "配置",
|
||||
"config.watermark.label": "水印",
|
||||
"config.webSearch.label": "联网搜索",
|
||||
"config.width.label": "宽度",
|
||||
"generation.actions.applySeed": "应用种子",
|
||||
"generation.actions.copyError": "复制错误信息",
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
"config.imageUrl.label": "起始画面",
|
||||
"config.prompt.placeholder": "描述你想生成的视频内容",
|
||||
"config.prompt.placeholderWithRef": "结合图片,描述你想生成的画面",
|
||||
"config.promptExtend.label": "提示词扩展",
|
||||
"config.referenceImage.label": "参考图像",
|
||||
"config.resolution.label": "分辨率",
|
||||
"config.seed.label": "种子",
|
||||
"config.seed.random": "随机",
|
||||
"config.size.label": "尺寸",
|
||||
"config.watermark.label": "水印",
|
||||
"config.webSearch.label": "联网搜索",
|
||||
"generation.actions.copyError": "复制错误信息",
|
||||
"generation.actions.errorCopied": "错误信息已复制到剪贴板",
|
||||
"generation.actions.errorCopyFailed": "复制错误信息失败",
|
||||
|
||||
@@ -886,8 +886,9 @@ const googleVideoModels: AIVideoModelCard[] = [
|
||||
endImageUrl: {
|
||||
default: null,
|
||||
},
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 3,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
resolution: {
|
||||
@@ -917,8 +918,9 @@ const googleVideoModels: AIVideoModelCard[] = [
|
||||
endImageUrl: {
|
||||
default: null,
|
||||
},
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 3,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
resolution: {
|
||||
|
||||
@@ -528,6 +528,8 @@ const hunyuanImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
|
||||
@@ -20,9 +20,9 @@ export const seedance20Params: VideoModelParamsSchema = {
|
||||
width: { max: 6000, min: 300 },
|
||||
},
|
||||
generateAudio: { default: true },
|
||||
imageUrl: {
|
||||
imageUrls: {
|
||||
aspectRatio: { max: 2.5, min: 0.4 },
|
||||
default: null,
|
||||
default: [],
|
||||
height: { max: 6000, min: 300 },
|
||||
maxFileSize: 30 * 1024 * 1024,
|
||||
width: { max: 6000, min: 300 },
|
||||
|
||||
@@ -265,6 +265,12 @@ const minimaxImageModels: AIImageModelCard[] = [
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0.025, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2025-02-28',
|
||||
type: 'image',
|
||||
@@ -285,6 +291,12 @@ const minimaxImageModels: AIImageModelCard[] = [
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0.025, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2025-02-28',
|
||||
type: 'image',
|
||||
@@ -308,6 +320,8 @@ const minimaxVideoModels: AIVideoModelCard[] = [
|
||||
default: '768P',
|
||||
enum: ['768P', '1080P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -332,6 +346,8 @@ const minimaxVideoModels: AIVideoModelCard[] = [
|
||||
default: '768P',
|
||||
enum: ['768P', '1080P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -359,6 +375,8 @@ const minimaxVideoModels: AIVideoModelCard[] = [
|
||||
default: '768P',
|
||||
enum: ['512P', '768P', '1080P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -382,6 +400,8 @@ const minimaxVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['720P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
releasedAt: '2025-02-11',
|
||||
type: 'video',
|
||||
@@ -398,6 +418,8 @@ const minimaxVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['720P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
releasedAt: '2025-02-11',
|
||||
type: 'video',
|
||||
@@ -416,6 +438,8 @@ const minimaxVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['720P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
releasedAt: '2025-03-03',
|
||||
type: 'video',
|
||||
@@ -434,6 +458,8 @@ const minimaxVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['720P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
releasedAt: '2025-03-03',
|
||||
type: 'video',
|
||||
@@ -447,6 +473,8 @@ const minimaxVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
releasedAt: '2025-01-10',
|
||||
type: 'video',
|
||||
@@ -462,6 +490,8 @@ const minimaxVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['720P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
releasedAt: '2025-03-03',
|
||||
type: 'video',
|
||||
|
||||
@@ -2909,6 +2909,7 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 4096, min: 256, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -2934,6 +2935,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 4096, min: 256, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -2959,6 +2962,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 4096, min: 256, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -2972,7 +2977,6 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
'Qwen Image Editing Model supports multi-image input and multi-image output, enabling precise in-image text editing, object addition, removal, or relocation, subject action modification, image style transfer, and enhanced visual detail.',
|
||||
displayName: 'Qwen Image Edit Max',
|
||||
id: 'qwen-image-edit-max',
|
||||
enabled: true,
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1536, max: 2048, min: 512, step: 1 },
|
||||
@@ -2984,6 +2988,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -2996,7 +3002,6 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
description:
|
||||
'Qwen Image Editing Model supports multi-image input and multi-image output, enabling precise in-image text editing, object addition, removal, or relocation, subject action modification, image style transfer, and enhanced visual detail.',
|
||||
displayName: 'Qwen Image Edit Plus',
|
||||
enabled: true,
|
||||
id: 'qwen-image-edit-plus',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
@@ -3009,6 +3014,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3021,7 +3028,6 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
description:
|
||||
'Qwen Image Edit is an image-to-image model that edits images based on input images and text prompts, enabling precise adjustments and creative transformations.',
|
||||
displayName: 'Qwen Image Edit',
|
||||
enabled: true,
|
||||
id: 'qwen-image-edit',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
@@ -3032,6 +3038,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3044,7 +3052,6 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
description:
|
||||
'Qwen Image Generation Model (Max series) delivers enhanced realism and visual naturalness compared with the Plus series, effectively reducing AI-generated artifacts, and demonstrating outstanding performance in human appearance, texture details, and text rendering.',
|
||||
displayName: 'Qwen Image Max',
|
||||
enabled: true,
|
||||
id: 'qwen-image-max',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
@@ -3056,6 +3063,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
default: '1664x928',
|
||||
enum: ['1664x928', '1472x1140', '1328x1328', '1140x1472', '928x1664'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3068,7 +3077,6 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
description:
|
||||
'It supports a wide range of artistic styles and is particularly proficient at rendering complex text within images, enabling integrated image–text layout design.',
|
||||
displayName: 'Qwen Image Plus',
|
||||
enabled: true,
|
||||
id: 'qwen-image-plus',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
@@ -3080,6 +3088,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
default: '1664x928',
|
||||
enum: ['1664x928', '1472x1140', '1328x1328', '1140x1472', '928x1664'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3092,7 +3102,6 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
description:
|
||||
'Qwen-Image is a general image generation model supporting multiple art styles and strong complex text rendering, especially Chinese and English. It supports multi-line layouts, paragraph-level text, and fine detail for complex text-image layouts.',
|
||||
displayName: 'Qwen Image',
|
||||
enabled: true,
|
||||
id: 'qwen-image',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
@@ -3104,6 +3113,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
default: '1328x1328',
|
||||
enum: ['1664x928', '1472x1140', '1328x1328', '1140x1472', '928x1664'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3128,6 +3139,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 2048, max: 11_585, min: 271, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3152,6 +3165,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 2048, max: 5792, min: 271, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3176,6 +3191,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1280, max: 2880, min: 640, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3198,6 +3215,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1280, max: 2880, min: 640, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3221,6 +3240,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1280, max: 2560, min: 384, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3242,6 +3263,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1280, max: 2880, min: 640, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3263,6 +3286,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3284,6 +3309,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3305,6 +3332,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3326,6 +3355,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3347,6 +3378,8 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 1440, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3397,6 +3430,7 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
default: '1k',
|
||||
enum: ['1k', '2k'],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3427,6 +3461,7 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
default: '1k',
|
||||
enum: ['1k', '2k', '4k'],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3435,162 +3470,6 @@ const qwenImageModels: AIImageModelCard[] = [
|
||||
releasedAt: '2026-03-26',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'FLUX.1 [schnell] is the most advanced open-source few-step model, surpassing similar competitors and even strong non-distilled models like Midjourney v6.0 and DALL-E 3 (HD). It is finely tuned to preserve pretraining diversity, significantly improving visual quality, instruction following, size/aspect variation, font handling, and output diversity.',
|
||||
displayName: 'FLUX.1 [schnell]',
|
||||
id: 'flux-schnell',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['512x1024', '768x512', '768x1024', '1024x576', '576x1024', '1024x1024'],
|
||||
},
|
||||
steps: { default: 4, max: 12, min: 1 },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2024-08-07',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'FLUX.1 [dev] is an open-weights distilled model for non-commercial use. It keeps near-pro image quality and instruction following while running more efficiently, using resources better than same-size standard models.',
|
||||
displayName: 'FLUX.1 [dev]',
|
||||
id: 'flux-dev',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['512x1024', '768x512', '768x1024', '1024x576', '576x1024', '1024x1024'],
|
||||
},
|
||||
steps: { default: 50, max: 50, min: 1 },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2024-08-07',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'FLUX.1 [merged] combines the deep features explored in "DEV" with the high-speed advantages of "Schnell", extending performance limits and broadening applications.',
|
||||
displayName: 'FLUX.1 [merged]',
|
||||
id: 'flux-merged',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['512x1024', '768x512', '768x1024', '1024x576', '576x1024', '1024x1024'],
|
||||
},
|
||||
steps: { default: 30, max: 30, min: 1 },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2024-08-22',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'stable-diffusion-3.5-large is an 800M-parameter MMDiT text-to-image model with excellent quality and prompt alignment, supporting 1-megapixel images and efficient runs on consumer hardware.',
|
||||
displayName: 'StableDiffusion 3.5 Large',
|
||||
id: 'stable-diffusion-3.5-large',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
steps: { default: 40, max: 500, min: 1 },
|
||||
width: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2024-10-25',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'stable-diffusion-3.5-large-turbo applies adversarial diffusion distillation (ADD) to stable-diffusion-3.5-large for faster speed.',
|
||||
displayName: 'StableDiffusion 3.5 Large Turbo',
|
||||
id: 'stable-diffusion-3.5-large-turbo',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
steps: { default: 40, max: 500, min: 1 },
|
||||
width: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2024-10-25',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'stable-diffusion-xl brings major improvements over v1.5 and matches top open text-to-image results. Improvements include a 3x larger UNet backbone, a refinement module for better image quality, and more efficient training techniques.',
|
||||
displayName: 'StableDiffusion xl',
|
||||
id: 'stable-diffusion-xl',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
steps: { default: 50, max: 500, min: 1 },
|
||||
width: { default: 1024, max: 1024, min: 512, step: 128 },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2024-04-09',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
description:
|
||||
'stable-diffusion-v1.5 is initialized from the v1.2 checkpoint and fine-tuned for 595k steps on "laion-aesthetics v2 5+" at 512x512 resolution, reducing text conditioning by 10% to improve classifier-free guidance sampling.',
|
||||
displayName: 'StableDiffusion v1.5',
|
||||
id: 'stable-diffusion-v1.5',
|
||||
organization: 'Qwen',
|
||||
parameters: {
|
||||
height: { default: 512, max: 1024, min: 512, step: 128 },
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
steps: { default: 50, max: 500, min: 1 },
|
||||
width: { default: 512, max: 1024, min: 512, step: 128 },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2024-04-09',
|
||||
type: 'image',
|
||||
},
|
||||
];
|
||||
|
||||
const qwenVideoModels: AIVideoModelCard[] = [
|
||||
@@ -3614,6 +3493,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3634,8 +3515,9 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['16:9', '9:16', '1:1', '4:3', '3:4'],
|
||||
},
|
||||
duration: { default: 5, max: 10, min: 2 },
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 5,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
resolution: {
|
||||
@@ -3643,6 +3525,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3669,6 +3553,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3695,6 +3581,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3719,6 +3607,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3736,8 +3626,9 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
parameters: {
|
||||
duration: { default: 5, max: 10, min: 2 },
|
||||
generateAudio: { default: true },
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 5,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
size: {
|
||||
@@ -3756,6 +3647,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
],
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3771,8 +3664,9 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
id: 'wan2.6-r2v',
|
||||
parameters: {
|
||||
duration: { default: 5, max: 10, min: 2 },
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 5,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
size: {
|
||||
@@ -3791,6 +3685,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
],
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3824,6 +3720,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
],
|
||||
},
|
||||
seed: { default: null },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3847,6 +3745,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
default: '1080P',
|
||||
enum: ['480P', '720P', '1080P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3881,6 +3781,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1248x1632',
|
||||
],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3906,6 +3808,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['480P', '720P', '1080P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3931,6 +3835,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['720P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3954,6 +3860,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['480P', '720P', '1080P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -3977,6 +3885,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
default: '1080P',
|
||||
enum: ['480P', '1080P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4006,6 +3916,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1248x1632',
|
||||
],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4028,6 +3940,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['480P', '720P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4051,6 +3965,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
default: '720P',
|
||||
enum: ['720P'],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4079,6 +3995,8 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'832x1088',
|
||||
],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4095,10 +4013,12 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
parameters: {
|
||||
duration: { default: 5, enum: [5] },
|
||||
prompt: { default: '' },
|
||||
promptExtend: { default: false },
|
||||
size: {
|
||||
default: '1280x720',
|
||||
enum: ['1280x720', '720x1280', '960x960', '1088x832', '832x1088'],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4131,6 +4051,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
default: '1080p',
|
||||
enum: ['720p', '1080p'],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4154,14 +4075,16 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
generateAudio: { default: true },
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 7,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
resolution: {
|
||||
default: '1080p',
|
||||
enum: ['720p', '1080p'],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4204,6 +4127,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1238x1674',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4245,6 +4169,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1238x1674',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4285,6 +4210,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1238x1674',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4310,6 +4236,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4334,6 +4261,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4357,6 +4285,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4380,6 +4309,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4408,6 +4338,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4435,6 +4366,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4461,6 +4393,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4487,6 +4420,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4501,8 +4435,9 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
id: 'vidu/viduq2-pro_reference2video',
|
||||
parameters: {
|
||||
duration: { default: 5, max: 10, min: 1 },
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 7,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
resolution: {
|
||||
@@ -4530,6 +4465,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1080x1920',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4544,8 +4480,9 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
id: 'vidu/viduq2_reference2video',
|
||||
parameters: {
|
||||
duration: { default: 5, max: 10, min: 1 },
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 7,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
resolution: {
|
||||
@@ -4573,6 +4510,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1080x1920',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4628,6 +4566,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1920x832',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4670,6 +4609,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1080x1920',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4695,6 +4635,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['360P', '540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4719,6 +4660,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['360P', '540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4747,6 +4689,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['360P', '540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4774,6 +4717,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['360P', '540P', '720P', '1080P'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -4789,8 +4733,9 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
parameters: {
|
||||
duration: { default: 5, enum: [5, 8, 10] },
|
||||
generateAudio: { default: true },
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 7,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
seed: { default: null },
|
||||
@@ -4819,6 +4764,7 @@ const qwenVideoModels: AIVideoModelCard[] = [
|
||||
'1080x1920',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
|
||||
@@ -1165,11 +1165,14 @@ const volcengineImageModels: AIImageModelCard[] = [
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
promptExtend: { default: 'off', enum: ['off', 'standard'] },
|
||||
watermark: { default: false },
|
||||
webSearch: { default: false },
|
||||
width: { default: 2048, max: 16_384, min: 480, step: 1 },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0, strategy: 'fixed', unit: 'image' }],
|
||||
units: [{ name: 'imageGeneration', rate: 0.22, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2026-01-28',
|
||||
type: 'image',
|
||||
@@ -1186,6 +1189,8 @@ const volcengineImageModels: AIImageModelCard[] = [
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
promptExtend: { default: 'off', enum: ['off', 'standard'] },
|
||||
watermark: { default: false },
|
||||
width: { default: 2048, max: 16_384, min: 480, step: 1 },
|
||||
},
|
||||
pricing: {
|
||||
@@ -1196,12 +1201,6 @@ const volcengineImageModels: AIImageModelCard[] = [
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
/*
|
||||
// TODO: AIImageModelCard does not support config.deploymentName
|
||||
config: {
|
||||
deploymentName: 'doubao-seedream-3-0-t2i-250415',
|
||||
},
|
||||
*/
|
||||
description:
|
||||
'Seedream 4.0 is an image generation model from ByteDance Seed, supporting text and image inputs with highly controllable, high-quality image generation. It generates images from text prompts.',
|
||||
displayName: 'Seedream 4.0',
|
||||
@@ -1213,6 +1212,8 @@ const volcengineImageModels: AIImageModelCard[] = [
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
promptExtend: { default: 'off', enum: ['off', 'standard', 'fast'] },
|
||||
watermark: { default: false },
|
||||
width: { default: 2048, max: 16_384, min: 240, step: 1 },
|
||||
},
|
||||
pricing: {
|
||||
@@ -1223,16 +1224,9 @@ const volcengineImageModels: AIImageModelCard[] = [
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
/*
|
||||
// TODO: AIImageModelCard does not support config.deploymentName
|
||||
config: {
|
||||
deploymentName: 'doubao-seedream-3-0-t2i-250415',
|
||||
},
|
||||
*/
|
||||
description:
|
||||
'Seedream 3.0 is an image generation model from ByteDance Seed, supporting text and image inputs with highly controllable, high-quality image generation. It generates images from text prompts.',
|
||||
displayName: 'Seedream 3.0 Text-to-Image',
|
||||
enabled: true,
|
||||
id: 'doubao-seedream-3-0-t2i-250415',
|
||||
parameters: {
|
||||
cfg: { default: 2.5, max: 10, min: 1, step: 0.1 },
|
||||
@@ -1241,6 +1235,7 @@ const volcengineImageModels: AIImageModelCard[] = [
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
width: { default: 1024, max: 3549, min: 296, step: 1 },
|
||||
},
|
||||
pricing: {
|
||||
@@ -1250,15 +1245,10 @@ const volcengineImageModels: AIImageModelCard[] = [
|
||||
releasedAt: '2025-04-15',
|
||||
type: 'image',
|
||||
},
|
||||
// Note: Doubao image-to-image and text-to-image models share the same Endpoint, currently switches to edit endpoint if imageUrl exists
|
||||
{
|
||||
// config: {
|
||||
// deploymentName: 'doubao-seededit-3-0-i2i-250628',
|
||||
// },
|
||||
description:
|
||||
'The Doubao image model from ByteDance Seed supports text and image inputs with highly controllable, high-quality image generation. It supports text-guided image editing, with output sizes between 512 and 1536 on the long side.',
|
||||
displayName: 'SeedEdit 3.0 Image-to-Image',
|
||||
enabled: true,
|
||||
id: 'doubao-seededit-3-0-i2i-250628',
|
||||
parameters: {
|
||||
cfg: { default: 5.5, max: 10, min: 1, step: 0.1 },
|
||||
@@ -1267,6 +1257,11 @@ const volcengineImageModels: AIImageModelCard[] = [
|
||||
default: '',
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'imageGeneration', rate: 0.259, strategy: 'fixed', unit: 'image' }],
|
||||
},
|
||||
releasedAt: '2025-06-28',
|
||||
type: 'image',
|
||||
@@ -1281,7 +1276,15 @@ const volcengineVideoModels: AIVideoModelCard[] = [
|
||||
enabled: true,
|
||||
id: 'doubao-seedance-2-0-260128',
|
||||
organization: 'ByteDance',
|
||||
parameters: seedance20Params,
|
||||
parameters: {
|
||||
...seedance20Params,
|
||||
watermark: { default: false },
|
||||
webSearch: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'videoGeneration', rate: 37, strategy: 'fixed', unit: 'millionTokens' }],
|
||||
},
|
||||
releasedAt: '2026-01-28',
|
||||
type: 'video',
|
||||
},
|
||||
@@ -1292,7 +1295,15 @@ const volcengineVideoModels: AIVideoModelCard[] = [
|
||||
enabled: true,
|
||||
id: 'doubao-seedance-2-0-fast-260128',
|
||||
organization: 'ByteDance',
|
||||
parameters: seedance20Params,
|
||||
parameters: {
|
||||
...seedance20Params,
|
||||
watermark: { default: false },
|
||||
webSearch: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'videoGeneration', rate: 46, strategy: 'fixed', unit: 'millionTokens' }],
|
||||
},
|
||||
releasedAt: '2026-01-28',
|
||||
type: 'video',
|
||||
},
|
||||
@@ -1303,7 +1314,24 @@ const volcengineVideoModels: AIVideoModelCard[] = [
|
||||
enabled: true,
|
||||
id: 'doubao-seedance-1-5-pro-251215',
|
||||
organization: 'ByteDance',
|
||||
parameters: seedance15ProParams,
|
||||
parameters: {
|
||||
...seedance15ProParams,
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [
|
||||
{
|
||||
lookup: {
|
||||
pricingParams: ['generateAudio'],
|
||||
prices: { false: 8, true: 16 },
|
||||
},
|
||||
name: 'videoGeneration',
|
||||
strategy: 'lookup',
|
||||
unit: 'millionTokens',
|
||||
},
|
||||
],
|
||||
},
|
||||
releasedAt: '2025-12-15',
|
||||
type: 'video',
|
||||
},
|
||||
@@ -1333,6 +1361,11 @@ const volcengineVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['480p', '720p', '1080p'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'videoGeneration', rate: 4.2, strategy: 'fixed', unit: 'millionTokens' }],
|
||||
},
|
||||
releasedAt: '2025-10-15',
|
||||
type: 'video',
|
||||
@@ -1371,6 +1404,11 @@ const volcengineVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['480p', '720p', '1080p'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'videoGeneration', rate: 15, strategy: 'fixed', unit: 'millionTokens' }],
|
||||
},
|
||||
releasedAt: '2025-05-28',
|
||||
type: 'video',
|
||||
@@ -1395,11 +1433,12 @@ const volcengineVideoModels: AIVideoModelCard[] = [
|
||||
requiresImageUrl: true,
|
||||
width: { max: 6000, min: 300 },
|
||||
},
|
||||
imageUrl: {
|
||||
imageUrls: {
|
||||
aspectRatio: { max: 2.5, min: 0.4 },
|
||||
default: null,
|
||||
default: [],
|
||||
height: { max: 6000, min: 300 },
|
||||
maxFileSize: 30 * 1024 * 1024,
|
||||
maxCount: 4,
|
||||
width: { max: 6000, min: 300 },
|
||||
},
|
||||
duration: { default: 5, max: 12, min: 2 },
|
||||
@@ -1409,6 +1448,11 @@ const volcengineVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['480p', '720p', '1080p'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'videoGeneration', rate: 10, strategy: 'fixed', unit: 'millionTokens' }],
|
||||
},
|
||||
releasedAt: '2025-04-28',
|
||||
type: 'video',
|
||||
@@ -1432,6 +1476,11 @@ const volcengineVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['480p', '720p', '1080p'],
|
||||
},
|
||||
seed: { default: null },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
units: [{ name: 'videoGeneration', rate: 10, strategy: 'fixed', unit: 'millionTokens' }],
|
||||
},
|
||||
releasedAt: '2025-04-28',
|
||||
type: 'video',
|
||||
|
||||
@@ -1772,6 +1772,7 @@ const wenxinImageModels: AIImageModelCard[] = [
|
||||
'1104x1472',
|
||||
],
|
||||
},
|
||||
promptExtend: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1791,6 +1792,7 @@ const wenxinImageModels: AIImageModelCard[] = [
|
||||
default: '',
|
||||
},
|
||||
width: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1812,6 +1814,7 @@ const wenxinImageModels: AIImageModelCard[] = [
|
||||
default: '',
|
||||
},
|
||||
width: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1834,6 +1837,8 @@ const wenxinImageModels: AIImageModelCard[] = [
|
||||
seed: { default: null },
|
||||
steps: { default: 25, max: 50, min: 1 },
|
||||
width: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1855,6 +1860,8 @@ const wenxinImageModels: AIImageModelCard[] = [
|
||||
},
|
||||
seed: { default: null },
|
||||
width: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1876,6 +1883,7 @@ const wenxinImageModels: AIImageModelCard[] = [
|
||||
seed: { default: null },
|
||||
steps: { default: 25, max: 50, min: 1 },
|
||||
width: { default: 1024, max: 2048, min: 512, step: 1 },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1899,6 +1907,8 @@ const wenxinVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1918,6 +1928,8 @@ const wenxinVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1937,6 +1949,8 @@ const wenxinVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1956,6 +1970,8 @@ const wenxinVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1974,6 +1990,8 @@ const wenxinVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
|
||||
@@ -1256,6 +1256,10 @@ const zhipuImageModels: AIImageModelCard[] = [
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
resolution: {
|
||||
default: 'hd',
|
||||
enum: ['hd'],
|
||||
},
|
||||
size: {
|
||||
default: '1280x1280',
|
||||
enum: [
|
||||
@@ -1268,6 +1272,7 @@ const zhipuImageModels: AIImageModelCard[] = [
|
||||
'960x1728',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1286,10 +1291,15 @@ const zhipuImageModels: AIImageModelCard[] = [
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
resolution: {
|
||||
default: 'standard',
|
||||
enum: ['hd', 'standard'],
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '768x1344', '864x1152', '1344x768', '1152x864', '1440x720', '720x1440'],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1308,10 +1318,15 @@ const zhipuImageModels: AIImageModelCard[] = [
|
||||
prompt: {
|
||||
default: '',
|
||||
},
|
||||
resolution: {
|
||||
default: 'standard',
|
||||
enum: ['hd', 'standard'],
|
||||
},
|
||||
size: {
|
||||
default: '1024x1024',
|
||||
enum: ['1024x1024', '768x1344', '864x1152', '1344x768', '1152x864', '1440x720', '720x1440'],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1334,12 +1349,10 @@ const zhipuVideoModels: AIVideoModelCard[] = [
|
||||
enum: ['16:9', '9:16', '1:1'],
|
||||
},
|
||||
duration: { default: 4, enum: [4] },
|
||||
endImageUrl: {
|
||||
default: null,
|
||||
},
|
||||
generateAudio: { default: true },
|
||||
imageUrl: {
|
||||
default: null,
|
||||
imageUrls: {
|
||||
default: [],
|
||||
maxCount: 3,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
size: {
|
||||
@@ -1507,6 +1520,10 @@ const zhipuVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
resolution: {
|
||||
default: 'speed',
|
||||
enum: ['speed', 'quality'],
|
||||
},
|
||||
size: {
|
||||
default: '1920x1080',
|
||||
enum: [
|
||||
@@ -1519,6 +1536,7 @@ const zhipuVideoModels: AIVideoModelCard[] = [
|
||||
'3840x2160',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1538,6 +1556,10 @@ const zhipuVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
resolution: {
|
||||
default: 'speed',
|
||||
enum: ['speed', 'quality'],
|
||||
},
|
||||
size: {
|
||||
default: '1920x1080',
|
||||
enum: [
|
||||
@@ -1551,6 +1573,7 @@ const zhipuVideoModels: AIVideoModelCard[] = [
|
||||
'3840x2160',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
@@ -1570,6 +1593,10 @@ const zhipuVideoModels: AIVideoModelCard[] = [
|
||||
default: null,
|
||||
},
|
||||
prompt: { default: '' },
|
||||
resolution: {
|
||||
default: 'speed',
|
||||
enum: ['speed', 'quality'],
|
||||
},
|
||||
size: {
|
||||
default: '1920x1080',
|
||||
enum: [
|
||||
@@ -1583,6 +1610,7 @@ const zhipuVideoModels: AIVideoModelCard[] = [
|
||||
'3840x2160',
|
||||
],
|
||||
},
|
||||
watermark: { default: false },
|
||||
},
|
||||
pricing: {
|
||||
currency: 'CNY',
|
||||
|
||||
@@ -11,7 +11,10 @@ describe('meta-schema', () => {
|
||||
width: { default: 1024, min: 512, max: 2048, step: 64 },
|
||||
height: { default: 1024, min: 512, max: 2048, step: 64 },
|
||||
steps: { default: 20, min: 1, max: 50 },
|
||||
promptExtend: { default: false },
|
||||
watermark: { default: false },
|
||||
seed: { default: null, min: 0 },
|
||||
webSearch: { default: true },
|
||||
cfg: { default: 7.5, min: 1, max: 20, step: 0.5 },
|
||||
aspectRatio: { default: '1:1', enum: ['1:1', '16:9', '4:3'] },
|
||||
size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] },
|
||||
@@ -34,15 +37,21 @@ describe('meta-schema', () => {
|
||||
const schema: ModelParamsSchema = {
|
||||
prompt: {},
|
||||
width: { default: 1024, min: 512, max: 2048 },
|
||||
promptExtend: { default: 'standard', enum: ['standard', 'fast'] },
|
||||
watermark: {},
|
||||
seed: {},
|
||||
webSearch: {},
|
||||
};
|
||||
|
||||
const result = ModelParamsMetaSchema.parse(schema);
|
||||
|
||||
expect(result.prompt.default).toBe('');
|
||||
expect(result.width?.step).toBe(1);
|
||||
expect(result.promptExtend?.default).toBe('standard');
|
||||
expect(result.watermark?.default).toBe(false);
|
||||
expect(result.seed?.default).toBeNull();
|
||||
expect(result.seed?.min).toBe(0);
|
||||
expect(result.webSearch?.default).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid parameter schemas', () => {
|
||||
@@ -148,6 +157,9 @@ describe('meta-schema', () => {
|
||||
prompt: { default: 'test' },
|
||||
width: { default: 1024, min: 512, max: 2048 },
|
||||
seed: { default: 12345 },
|
||||
promptExtend: { default: 'fast', enum: ['standard', 'fast'] },
|
||||
watermark: { default: true },
|
||||
webSearch: { default: false },
|
||||
cfg: { default: 7.5, min: 1, max: 20, step: 0.5 },
|
||||
aspectRatio: { default: '16:9', enum: ['1:1', '16:9', '4:3'] },
|
||||
imageUrls: { default: ['test.jpg'] },
|
||||
@@ -159,6 +171,9 @@ describe('meta-schema', () => {
|
||||
expect(typeof result.prompt).toBe('string');
|
||||
expect(typeof result.width).toBe('number');
|
||||
expect(typeof result.seed).toBe('number');
|
||||
expect(typeof result.promptExtend).toBe('string');
|
||||
expect(typeof result.watermark).toBe('boolean');
|
||||
expect(typeof result.webSearch).toBe('boolean');
|
||||
expect(typeof result.cfg).toBe('number');
|
||||
expect(typeof result.aspectRatio).toBe('string');
|
||||
expect(Array.isArray(result.imageUrls)).toBe(true);
|
||||
|
||||
@@ -202,6 +202,31 @@ export const ModelParamsMetaSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
|
||||
promptExtend: z
|
||||
.object({
|
||||
default: z.union([z.boolean(), z.string()]),
|
||||
description: z.string().optional(),
|
||||
enum: z.array(z.string()).optional(),
|
||||
type: z.union([z.literal('boolean'), z.literal('string')]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
watermark: z
|
||||
.object({
|
||||
default: z.boolean().default(false),
|
||||
description: z.string().optional(),
|
||||
type: z.literal('boolean').optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
webSearch: z
|
||||
.object({
|
||||
default: z.boolean().default(true),
|
||||
description: z.string().optional(),
|
||||
type: z.literal('boolean').optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
seed: z
|
||||
.object({
|
||||
default: z.number().nullable().default(null),
|
||||
|
||||
@@ -20,6 +20,9 @@ describe('video standard-parameters', () => {
|
||||
duration: { default: 5, max: 10, min: 1, step: 1 },
|
||||
endImageUrl: { default: null },
|
||||
generateAudio: { default: true },
|
||||
promptExtend: { default: 'standard', enum: ['standard', 'fast'] },
|
||||
watermark: { default: false },
|
||||
webSearch: { default: true },
|
||||
imageUrl: { default: null },
|
||||
prompt: { default: '' },
|
||||
resolution: { default: '720p', enum: ['480p', '720p', '1080p'] },
|
||||
@@ -41,6 +44,9 @@ describe('video standard-parameters', () => {
|
||||
const schema: VideoModelParamsSchema = {
|
||||
cameraFixed: {},
|
||||
generateAudio: {},
|
||||
promptExtend: { default: true },
|
||||
watermark: {},
|
||||
webSearch: {},
|
||||
prompt: {},
|
||||
seed: {},
|
||||
};
|
||||
@@ -50,6 +56,9 @@ describe('video standard-parameters', () => {
|
||||
expect(result.prompt.default).toBe('');
|
||||
expect(result.cameraFixed?.default).toBe(false);
|
||||
expect(result.generateAudio?.default).toBe(true);
|
||||
expect(result.promptExtend?.default).toBe(true);
|
||||
expect(result.watermark?.default).toBe(false);
|
||||
expect(result.webSearch?.default).toBe(true);
|
||||
expect(result.seed?.default).toBeNull();
|
||||
expect(result.seed?.max).toBe(MAX_VIDEO_SEED);
|
||||
expect(result.seed?.min).toBe(-1);
|
||||
@@ -89,6 +98,9 @@ describe('video standard-parameters', () => {
|
||||
cameraFixed: { default: true },
|
||||
duration: { default: 5, max: 10, min: 1 },
|
||||
generateAudio: { default: false },
|
||||
promptExtend: { default: 'fast', enum: ['standard', 'fast'] },
|
||||
watermark: { default: true },
|
||||
webSearch: { default: false },
|
||||
prompt: { default: 'test prompt' },
|
||||
resolution: { default: '1080p', enum: ['720p', '1080p'] },
|
||||
seed: { default: 42 },
|
||||
@@ -101,6 +113,9 @@ describe('video standard-parameters', () => {
|
||||
expect(result.cameraFixed).toBe(true);
|
||||
expect(result.duration).toBe(5);
|
||||
expect(result.generateAudio).toBe(false);
|
||||
expect(result.promptExtend).toBe('fast');
|
||||
expect(result.watermark).toBe(true);
|
||||
expect(result.webSearch).toBe(false);
|
||||
expect(result.resolution).toBe('1080p');
|
||||
expect(result.seed).toBe(42);
|
||||
});
|
||||
@@ -132,6 +147,9 @@ describe('video standard-parameters', () => {
|
||||
const schema: VideoModelParamsSchema = {
|
||||
cameraFixed: { default: false },
|
||||
generateAudio: { default: true },
|
||||
promptExtend: { default: 'standard', enum: ['standard', 'fast'] },
|
||||
watermark: { default: false },
|
||||
webSearch: { default: true },
|
||||
prompt: { default: 'hello' },
|
||||
seed: { default: null },
|
||||
};
|
||||
@@ -141,6 +159,9 @@ describe('video standard-parameters', () => {
|
||||
expect(typeof result.prompt).toBe('string');
|
||||
expect(typeof result.cameraFixed).toBe('boolean');
|
||||
expect(typeof result.generateAudio).toBe('boolean');
|
||||
expect(typeof result.promptExtend).toBe('string');
|
||||
expect(typeof result.watermark).toBe('boolean');
|
||||
expect(typeof result.webSearch).toBe('boolean');
|
||||
expect(result.seed).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -172,6 +193,9 @@ describe('video standard-parameters', () => {
|
||||
expect(params.prompt).toBe('required prompt');
|
||||
expect(params.cameraFixed).toBeUndefined();
|
||||
expect(params.generateAudio).toBeUndefined();
|
||||
expect(params.promptExtend).toBeUndefined();
|
||||
expect(params.watermark).toBeUndefined();
|
||||
expect(params.webSearch).toBeUndefined();
|
||||
expect(params.seed).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,6 +41,20 @@ export const VideoModelParamsMetaSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
|
||||
imageUrls: z
|
||||
.object({
|
||||
/** Aspect ratio (width/height) constraints */
|
||||
aspectRatio: z.object({ max: z.number().optional(), min: z.number().optional() }).optional(),
|
||||
default: z.array(z.string()),
|
||||
description: z.string().optional(),
|
||||
height: z.object({ max: z.number().optional(), min: z.number().optional() }).optional(),
|
||||
maxCount: z.number().optional(),
|
||||
maxFileSize: z.number().optional(),
|
||||
type: z.literal('array').optional(),
|
||||
width: z.object({ max: z.number().optional(), min: z.number().optional() }).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
endImageUrl: z
|
||||
.object({
|
||||
/** Aspect ratio (width/height) constraints */
|
||||
@@ -110,6 +124,31 @@ export const VideoModelParamsMetaSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
|
||||
promptExtend: z
|
||||
.object({
|
||||
default: z.union([z.boolean(), z.string()]),
|
||||
description: z.string().optional(),
|
||||
enum: z.array(z.string()).optional(),
|
||||
type: z.union([z.literal('boolean'), z.literal('string')]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
watermark: z
|
||||
.object({
|
||||
default: z.boolean().default(false),
|
||||
description: z.string().optional(),
|
||||
type: z.literal('boolean').optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
webSearch: z
|
||||
.object({
|
||||
default: z.boolean().default(true),
|
||||
description: z.string().optional(),
|
||||
type: z.literal('boolean').optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
seed: z
|
||||
.object({
|
||||
default: z.number().nullable().default(null),
|
||||
@@ -140,8 +179,13 @@ type VideoTypeMapping<T> = T extends 'string'
|
||||
type VideoTypeType<K extends VideoModelParamsKeys> = NonNullable<
|
||||
VideoModelParamsOutputSchema[K]
|
||||
>['type'];
|
||||
type VideoDefaultType<K extends VideoModelParamsKeys> = NonNullable<
|
||||
VideoModelParamsOutputSchema[K]
|
||||
>['default'];
|
||||
type _StandardVideoGenerationParameters<P extends VideoModelParamsKeys = VideoModelParamsKeys> = {
|
||||
[key in P]: VideoTypeMapping<VideoTypeType<key>>;
|
||||
[key in P]: NonNullable<VideoTypeType<key>> extends 'array'
|
||||
? VideoDefaultType<key>
|
||||
: VideoTypeMapping<VideoTypeType<key>>;
|
||||
};
|
||||
|
||||
export type RuntimeVideoGenParams = Pick<_StandardVideoGenerationParameters, 'prompt'> &
|
||||
|
||||
@@ -49,6 +49,7 @@ export async function createGoogleVideo(
|
||||
const {
|
||||
prompt,
|
||||
imageUrl,
|
||||
imageUrls,
|
||||
endImageUrl,
|
||||
aspectRatio,
|
||||
duration,
|
||||
@@ -76,6 +77,18 @@ export async function createGoogleVideo(
|
||||
...(config && { config }),
|
||||
};
|
||||
|
||||
if (imageUrls && imageUrls.length > 0) {
|
||||
if (imageUrls.length === 1) {
|
||||
requestParams.image = await imageToGoogleImageFormat(imageUrls[0]);
|
||||
} else {
|
||||
requestParams.config.referenceImages = await Promise.all(
|
||||
imageUrls.map(async (url) => ({
|
||||
image: await imageToGoogleImageFormat(url),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log('Google video generation request params: %O', requestParams);
|
||||
|
||||
const operation = await client.models.generateVideos(requestParams);
|
||||
|
||||
@@ -67,6 +67,7 @@ describe('createHunyuanImage', () => {
|
||||
size: '1024:1024',
|
||||
extra_body: {
|
||||
logo_add: 0,
|
||||
revise: 0,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -71,7 +71,8 @@ export async function createHunyuanImage(
|
||||
? { images: [params.imageUrl] }
|
||||
: {}),
|
||||
extra_body: {
|
||||
logo_add: 0, // Add Watermark: 0 disabled, 1 enabled
|
||||
revise: params.promptExtend === true ? 1 : 0, // Prompt optimization switch, default is 0 (no optimization)
|
||||
logo_add: params.watermark === true ? 1 : 0, // Watermark switch, default is 0 (no watermark)
|
||||
...(typeof params.seed === 'number' ? { seed: params.seed } : {}),
|
||||
},
|
||||
};
|
||||
@@ -92,7 +93,9 @@ export async function createHunyuanImage(
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await submitResponse.json();
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
void error;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
typeof errorData?.error?.message === 'string'
|
||||
@@ -202,7 +205,9 @@ export async function createHunyuanImage(
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await queryResponse.json();
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
void error;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
typeof errorData?.message === 'string'
|
||||
|
||||
@@ -66,6 +66,8 @@ describe('createMiniMaxImage', () => {
|
||||
model: 'image-01',
|
||||
n: 1,
|
||||
prompt: 'A beautiful sunset over the mountains',
|
||||
aigc_watermark: false,
|
||||
prompt_optimizer: false,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -113,6 +115,8 @@ describe('createMiniMaxImage', () => {
|
||||
model: 'image-01',
|
||||
n: 1,
|
||||
prompt: 'Abstract digital art',
|
||||
aigc_watermark: false,
|
||||
prompt_optimizer: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -161,6 +165,8 @@ describe('createMiniMaxImage', () => {
|
||||
model: 'image-01',
|
||||
n: 1,
|
||||
prompt: 'Reproducible image with seed',
|
||||
aigc_watermark: false,
|
||||
prompt_optimizer: false,
|
||||
seed: 42,
|
||||
}),
|
||||
}),
|
||||
@@ -211,6 +217,8 @@ describe('createMiniMaxImage', () => {
|
||||
model: 'image-01',
|
||||
n: 1,
|
||||
prompt: 'Image with seed 0',
|
||||
aigc_watermark: false,
|
||||
prompt_optimizer: false,
|
||||
seed: 0,
|
||||
}),
|
||||
}),
|
||||
@@ -331,6 +339,8 @@ describe('createMiniMaxImage', () => {
|
||||
model: 'image-01',
|
||||
n: 1,
|
||||
prompt: 'A girl looking into the distance from a library window',
|
||||
aigc_watermark: false,
|
||||
prompt_optimizer: false,
|
||||
subject_reference: [
|
||||
{
|
||||
type: 'character',
|
||||
@@ -389,6 +399,8 @@ describe('createMiniMaxImage', () => {
|
||||
model: 'image-01',
|
||||
n: 1,
|
||||
prompt: 'A girl looking into the distance from a library window',
|
||||
aigc_watermark: false,
|
||||
prompt_optimizer: false,
|
||||
subject_reference: referenceImageUrls.map((url) => ({
|
||||
type: 'character',
|
||||
image_file: url,
|
||||
|
||||
@@ -39,7 +39,8 @@ export async function createMiniMaxImage(
|
||||
model,
|
||||
n: 1,
|
||||
prompt: params.prompt,
|
||||
//prompt_optimizer: true, // Enable automatic prompt optimization
|
||||
aigc_watermark: params.watermark ?? false,
|
||||
prompt_optimizer: params.promptExtend ?? false,
|
||||
...(typeof params.seed === 'number' ? { seed: params.seed } : {}),
|
||||
};
|
||||
|
||||
|
||||
@@ -143,7 +143,8 @@ export async function createMiniMaxVideo(
|
||||
const body: Record<string, unknown> = {
|
||||
model,
|
||||
prompt,
|
||||
aigc_watermark: false, // Disable watermark for better user experience
|
||||
aigc_watermark: params.watermark ?? false,
|
||||
prompt_optimizer: params.promptExtend ?? false,
|
||||
...(typeof duration === 'number' ? { duration } : {}),
|
||||
...(typeof resolution === 'string' ? { resolution } : {}),
|
||||
};
|
||||
@@ -166,6 +167,8 @@ export async function createMiniMaxVideo(
|
||||
body.last_frame_image = endImageUrl;
|
||||
}
|
||||
|
||||
log('Creating video with MiniMax API - model: %s, params: %O', model, params);
|
||||
|
||||
const response = await fetch(`${baseURL}/video_generation`, {
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
|
||||
@@ -88,6 +88,8 @@ async function createLegacySynthesisTask(
|
||||
: params.size
|
||||
? { size: params.size.replaceAll('x', '*') }
|
||||
: { size: '1024*1024' }),
|
||||
...(params.promptExtend && { prompt_extend: params.promptExtend }),
|
||||
...(params.watermark && { watermark: params.watermark }),
|
||||
};
|
||||
|
||||
if (endpoint === 'image2image') {
|
||||
@@ -178,6 +180,8 @@ async function createHTTPAsyncGenerationTask(
|
||||
: params.size
|
||||
? { size: params.size.replaceAll('x', '*') }
|
||||
: { size: '1024*1024' }),
|
||||
...(params.promptExtend && { prompt_extend: params.promptExtend }),
|
||||
...(params.watermark && { watermark: params.watermark }),
|
||||
};
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
@@ -266,6 +270,8 @@ async function createHTTPSyncGeneration(
|
||||
parameters: {
|
||||
n: 1,
|
||||
...(typeof params.seed === 'number' ? { seed: params.seed } : {}),
|
||||
...(params.promptExtend && { prompt_extend: params.promptExtend }),
|
||||
...(params.watermark && { watermark: params.watermark }),
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
|
||||
@@ -117,7 +117,7 @@ async function createVideoTask(
|
||||
baseUrl: string,
|
||||
): Promise<string> {
|
||||
const { model, params } = payload;
|
||||
const { prompt, imageUrl, endImageUrl } = params;
|
||||
const { prompt, imageUrl, imageUrls, endImageUrl } = params;
|
||||
|
||||
// Determine the endpoint based on task type
|
||||
const url = `${baseUrl}/api/v1/services/aigc/${taskType}/video-synthesis`;
|
||||
@@ -158,6 +158,14 @@ async function createVideoTask(
|
||||
url: imageUrl,
|
||||
});
|
||||
}
|
||||
if (imageUrls && imageUrls.length > 0) {
|
||||
imageUrls.forEach((url) =>
|
||||
media.push({
|
||||
type: 'image',
|
||||
url,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (endImageUrl) {
|
||||
media.push({
|
||||
type: 'image',
|
||||
@@ -175,49 +183,19 @@ async function createVideoTask(
|
||||
url: imageUrl,
|
||||
});
|
||||
}
|
||||
if (endImageUrl) {
|
||||
media.push({
|
||||
type: 'last_frame',
|
||||
url: endImageUrl,
|
||||
});
|
||||
}
|
||||
if (media.length > 0) {
|
||||
input.media = media;
|
||||
}
|
||||
} else if (model.startsWith('pixverse/')) {
|
||||
if (imageUrl && !endImageUrl) {
|
||||
input.media = [
|
||||
{
|
||||
type: 'image_url',
|
||||
url: imageUrl,
|
||||
},
|
||||
];
|
||||
} else if (imageUrl && endImageUrl) {
|
||||
input.media = [
|
||||
{
|
||||
type: 'first_frame',
|
||||
url: imageUrl,
|
||||
},
|
||||
{
|
||||
type: 'last_frame',
|
||||
url: endImageUrl,
|
||||
},
|
||||
];
|
||||
}
|
||||
} else if (model.startsWith('wan2.7')) {
|
||||
const media = [];
|
||||
if (imageUrl) {
|
||||
if (model.includes('r2v')) {
|
||||
// For Wan2.7 R2V models, treat reference images as "reference_image" type to provide stronger referencing capability
|
||||
if (imageUrls && imageUrls.length > 0) {
|
||||
if (imageUrls.length === 1 && endImageUrl) {
|
||||
media.push({
|
||||
type: 'reference_image',
|
||||
url: imageUrl,
|
||||
type: 'first_frame',
|
||||
url: imageUrls[0],
|
||||
});
|
||||
} else {
|
||||
media.push({
|
||||
type: 'first_frame',
|
||||
url: imageUrl,
|
||||
});
|
||||
imageUrls.forEach((url) =>
|
||||
media.push({
|
||||
type: 'refer',
|
||||
url,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (endImageUrl) {
|
||||
@@ -229,12 +207,76 @@ async function createVideoTask(
|
||||
if (media.length > 0) {
|
||||
input.media = media;
|
||||
}
|
||||
} else if (model.startsWith('pixverse/')) {
|
||||
const media = [];
|
||||
if (imageUrls && imageUrls.length > 0) {
|
||||
imageUrls.forEach((url) =>
|
||||
media.push({
|
||||
type: 'image_url',
|
||||
url,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (imageUrl && !endImageUrl) {
|
||||
media.push({
|
||||
type: 'image_url',
|
||||
url: imageUrl,
|
||||
});
|
||||
} else if (imageUrl && endImageUrl) {
|
||||
media.push(
|
||||
{
|
||||
type: 'first_frame',
|
||||
url: imageUrl,
|
||||
},
|
||||
{
|
||||
type: 'last_frame',
|
||||
url: endImageUrl,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (media.length > 0) {
|
||||
input.media = media;
|
||||
}
|
||||
} else if (model.startsWith('wan2.7')) {
|
||||
const media = [];
|
||||
if (imageUrl) {
|
||||
media.push({
|
||||
type: 'first_frame',
|
||||
url: imageUrl,
|
||||
});
|
||||
}
|
||||
if (imageUrls && imageUrls.length > 0) {
|
||||
imageUrls.forEach((url) =>
|
||||
media.push({
|
||||
type: 'reference_image',
|
||||
url,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (endImageUrl) {
|
||||
media.push({
|
||||
type: 'last_frame',
|
||||
url: endImageUrl,
|
||||
});
|
||||
}
|
||||
if (media.length > 0) {
|
||||
input.media = media;
|
||||
}
|
||||
} else if (matchesModelPattern(model, reference2VideoModels)) {
|
||||
input.reference_urls = [imageUrl];
|
||||
if (imageUrl) {
|
||||
input.reference_urls = [imageUrl];
|
||||
}
|
||||
if (imageUrls && imageUrls.length > 0) {
|
||||
input.reference_urls = imageUrls;
|
||||
}
|
||||
} else if (matchesModelPattern(model, keyframe2VideoModels)) {
|
||||
input.first_frame_url = imageUrl;
|
||||
input.last_frame_url = endImageUrl;
|
||||
} else if (matchesModelPattern(model, image2VideoModels)) {
|
||||
if (imageUrl) {
|
||||
input.first_frame_url = imageUrl;
|
||||
}
|
||||
if (endImageUrl) {
|
||||
input.last_frame_url = endImageUrl;
|
||||
}
|
||||
} else if (matchesModelPattern(model, image2VideoModels) && imageUrl) {
|
||||
input.img_url = imageUrl;
|
||||
}
|
||||
|
||||
@@ -273,6 +315,14 @@ async function createVideoTask(
|
||||
}
|
||||
}
|
||||
|
||||
if (params.promptExtend) {
|
||||
parameters.prompt_extend = params.promptExtend;
|
||||
}
|
||||
|
||||
if (params.watermark) {
|
||||
parameters.watermark = params.watermark;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
body: JSON.stringify({
|
||||
input,
|
||||
|
||||
@@ -425,6 +425,128 @@ describe('createVolcengineImage', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow overriding watermark when watermark is provided', async () => {
|
||||
const mockResponse = {
|
||||
data: [{ url: 'https://example.com/test.jpg' }],
|
||||
};
|
||||
mockGenerate.mockResolvedValue(mockResponse);
|
||||
|
||||
payload.params = {
|
||||
prompt: 'test prompt',
|
||||
watermark: true,
|
||||
};
|
||||
|
||||
await createVolcengineImage(payload, options);
|
||||
|
||||
expect(mockGenerate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
watermark: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should enable web search tool when webSearch is true', async () => {
|
||||
const mockResponse = {
|
||||
data: [{ url: 'https://example.com/test.jpg' }],
|
||||
};
|
||||
mockGenerate.mockResolvedValue(mockResponse);
|
||||
|
||||
payload.model = 'doubao-seedream-5-0-260128';
|
||||
payload.params.webSearch = true;
|
||||
|
||||
await createVolcengineImage(payload, options);
|
||||
|
||||
expect(mockGenerate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tools: [{ type: 'web_search' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable web search tool by default when webSearch is undefined', async () => {
|
||||
const mockResponse = {
|
||||
data: [{ url: 'https://example.com/test.jpg' }],
|
||||
};
|
||||
mockGenerate.mockResolvedValue(mockResponse);
|
||||
|
||||
await createVolcengineImage(payload, options);
|
||||
|
||||
expect(mockGenerate).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
tools: [{ type: 'web_search' }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable web search tool when webSearch is false', async () => {
|
||||
const mockResponse = {
|
||||
data: [{ url: 'https://example.com/test.jpg' }],
|
||||
};
|
||||
mockGenerate.mockResolvedValue(mockResponse);
|
||||
|
||||
payload.params = {
|
||||
prompt: 'test prompt',
|
||||
webSearch: false,
|
||||
};
|
||||
|
||||
await createVolcengineImage(payload, options);
|
||||
|
||||
const requestOptions = mockGenerate.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(requestOptions.tools).toBeUndefined();
|
||||
expect(requestOptions.webSearch).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should add optimize_prompt_options if promptExtend is provided and not "off"', async () => {
|
||||
const mockResponse = {
|
||||
data: [{ url: 'https://example.com/test.jpg' }],
|
||||
};
|
||||
mockGenerate.mockResolvedValue(mockResponse);
|
||||
|
||||
payload.params = {
|
||||
prompt: 'test prompt',
|
||||
promptExtend: 'fast',
|
||||
};
|
||||
|
||||
await createVolcengineImage(payload, options);
|
||||
|
||||
const requestOptions = mockGenerate.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(requestOptions.optimize_prompt_options).toEqual({ mode: 'fast' });
|
||||
expect(requestOptions.promptExtend).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not add optimize_prompt_options if promptExtend is "off"', async () => {
|
||||
const mockResponse = {
|
||||
data: [{ url: 'https://example.com/test.jpg' }],
|
||||
};
|
||||
mockGenerate.mockResolvedValue(mockResponse);
|
||||
|
||||
payload.params = {
|
||||
prompt: 'test prompt',
|
||||
promptExtend: 'off',
|
||||
};
|
||||
|
||||
await createVolcengineImage(payload, options);
|
||||
|
||||
const requestOptions = mockGenerate.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(requestOptions.optimize_prompt_options).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not add optimize_prompt_options if promptExtend is undefined', async () => {
|
||||
const mockResponse = {
|
||||
data: [{ url: 'https://example.com/test.jpg' }],
|
||||
};
|
||||
mockGenerate.mockResolvedValue(mockResponse);
|
||||
|
||||
payload.params = {
|
||||
prompt: 'test prompt',
|
||||
};
|
||||
|
||||
await createVolcengineImage(payload, options);
|
||||
|
||||
const requestOptions = mockGenerate.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(requestOptions.optimize_prompt_options).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('size extraction', () => {
|
||||
|
||||
@@ -68,10 +68,19 @@ export async function createVolcengineImage(
|
||||
delete userInput.image;
|
||||
}
|
||||
|
||||
// Remove promptExtend and webSearch parameters that are not supported by Volcengine API
|
||||
delete userInput.promptExtend;
|
||||
delete userInput.webSearch;
|
||||
|
||||
// Build request options
|
||||
const requestOptions = {
|
||||
model,
|
||||
watermark: false, // Default to no watermark
|
||||
watermark: params.watermark ?? false, // Default to no watermark
|
||||
...(params.webSearch && { tools: [{ type: 'web_search' }] }),
|
||||
...(params.promptExtend &&
|
||||
params.promptExtend !== 'off' && {
|
||||
optimize_prompt_options: { mode: params.promptExtend },
|
||||
}),
|
||||
...userInput,
|
||||
};
|
||||
|
||||
|
||||
@@ -162,6 +162,29 @@ describe('createVolcengineVideo', () => {
|
||||
expect(body.generate_audio).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable web search tool by default when webSearch is undefined', async () => {
|
||||
payload.model = 'doubao-seedance-2-0-fast-260128';
|
||||
await createVolcengineVideo(payload, options);
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.tools).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should enable web search tool when webSearch is true', async () => {
|
||||
payload.model = 'doubao-seedance-2-0-fast-260128';
|
||||
payload.params.webSearch = true;
|
||||
await createVolcengineVideo(payload, options);
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.tools).toEqual([{ type: 'web_search' }]);
|
||||
});
|
||||
|
||||
it('should disable web search tool when webSearch is false', async () => {
|
||||
payload.model = 'doubao-seedance-2-0-fast-260128';
|
||||
payload.params.webSearch = false;
|
||||
await createVolcengineVideo(payload, options);
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.tools).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should map seed to body.seed', async () => {
|
||||
payload.params.seed = 42;
|
||||
await createVolcengineVideo(payload, options);
|
||||
@@ -196,6 +219,13 @@ describe('createVolcengineVideo', () => {
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.callback_url).toBe('https://example.com/webhook');
|
||||
});
|
||||
|
||||
it('should allow overriding watermark when watermark is provided', async () => {
|
||||
payload.params.watermark = true;
|
||||
await createVolcengineVideo(payload, options);
|
||||
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||
expect(body.watermark).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('client config', () => {
|
||||
|
||||
@@ -17,10 +17,13 @@ export async function createVolcengineVideo(
|
||||
const {
|
||||
prompt,
|
||||
imageUrl,
|
||||
imageUrls,
|
||||
endImageUrl,
|
||||
aspectRatio,
|
||||
duration,
|
||||
generateAudio,
|
||||
webSearch,
|
||||
watermark,
|
||||
seed,
|
||||
resolution,
|
||||
cameraFixed,
|
||||
@@ -37,6 +40,16 @@ export async function createVolcengineVideo(
|
||||
content.push({ image_url: { url: imageUrl }, role: 'first_frame', type: 'image_url' });
|
||||
}
|
||||
|
||||
if (imageUrls && imageUrls.length > 0) {
|
||||
if (imageUrls.length === 1 && endImageUrl) {
|
||||
content.push({ image_url: { url: imageUrls[0] }, role: 'first_frame', type: 'image_url' });
|
||||
} else {
|
||||
imageUrls.forEach((url) =>
|
||||
content.push({ image_url: { url }, role: 'reference_image', type: 'image_url' }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (endImageUrl) {
|
||||
content.push({ image_url: { url: endImageUrl }, role: 'last_frame', type: 'image_url' });
|
||||
}
|
||||
@@ -45,7 +58,8 @@ export async function createVolcengineVideo(
|
||||
const body: Record<string, unknown> = {
|
||||
content,
|
||||
model,
|
||||
watermark: false,
|
||||
watermark: watermark ?? false,
|
||||
...(webSearch && { tools: [{ type: 'web_search' }] }),
|
||||
};
|
||||
|
||||
if (aspectRatio !== undefined) body.ratio = aspectRatio;
|
||||
|
||||
@@ -56,6 +56,8 @@ export async function createWenxinImage(
|
||||
: {}),
|
||||
...(params.steps !== undefined && { steps: params.steps }),
|
||||
...(model === 'ernie-irag-edit' && { feature: 'variation' }),
|
||||
...(params.promptExtend && { prompt_extend: params.promptExtend }),
|
||||
...(params.watermark && { watermark: params.watermark }),
|
||||
};
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
@@ -71,7 +73,9 @@ export async function createWenxinImage(
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
void error;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
typeof errorData?.error === 'string'
|
||||
|
||||
@@ -91,7 +91,8 @@ export async function createWenxinVideo(
|
||||
options: CreateVideoOptions,
|
||||
): Promise<CreateVideoResponse> {
|
||||
const { model, params } = payload;
|
||||
const { prompt, imageUrl, aspectRatio, duration, generateAudio } = params;
|
||||
const { prompt, imageUrl, aspectRatio, duration, generateAudio, promptExtend, watermark } =
|
||||
params;
|
||||
|
||||
log('Creating video with Wenxin API - model: %s, params: %O', model, params);
|
||||
|
||||
@@ -125,6 +126,8 @@ export async function createWenxinVideo(
|
||||
if (aspectRatio) body.aspect_ratio = aspectRatio;
|
||||
if (duration) body.duration = duration;
|
||||
if (generateAudio !== undefined) body.generate_audio = generateAudio;
|
||||
if (promptExtend) body.prompt_extend = promptExtend;
|
||||
if (watermark) body.watermark = watermark;
|
||||
|
||||
log('Wenxin video API request body: %O', body);
|
||||
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { CreateImageOptions } from '../../core/openaiCompatibleFactory';
|
||||
import type { CreateImagePayload } from '../../types/image';
|
||||
import { createZhipuImage, pollZhipuImageStatus, queryZhipuImageStatus } from './createImage';
|
||||
|
||||
vi.mock('debug', () => ({
|
||||
default: vi.fn(() => vi.fn()),
|
||||
}));
|
||||
|
||||
describe('createZhipuImage', () => {
|
||||
const mockOptions: CreateImageOptions = {
|
||||
apiKey: 'test-api-key',
|
||||
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
provider: 'zhipu',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should create image with basic prompt', async () => {
|
||||
const mockTaskId = 'zhipu-task-123';
|
||||
const mockImageUrl = 'https://cdn.zhipu.ai/image.png';
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ id: mockTaskId }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
task_status: 'SUCCESS',
|
||||
image_result: [{ url: mockImageUrl }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'glm-image',
|
||||
params: {
|
||||
prompt: 'A cute cat',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await createZhipuImage(payload, mockOptions);
|
||||
|
||||
expect(fetch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://open.bigmodel.cn/api/paas/v4/async/images/generations',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-api-key',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const submitBody = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(submitBody).toEqual({
|
||||
model: 'glm-image',
|
||||
prompt: 'A cute cat',
|
||||
watermark_enabled: false,
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://open.bigmodel.cn/api/paas/v4/async-result/zhipu-task-123',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-api-key',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
imageUrl: mockImageUrl,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create image via sync endpoint for cogview-4 model', async () => {
|
||||
const mockImageUrl = 'https://cdn.zhipu.ai/sync-image.png';
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
created: 123,
|
||||
data: [{ url: mockImageUrl }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'cogview-4',
|
||||
params: {
|
||||
prompt: 'A cute cat on window',
|
||||
resolution: 'standard',
|
||||
size: '1280x1280',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await createZhipuImage(payload, mockOptions);
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
'https://open.bigmodel.cn/api/paas/v4/images/generations',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-api-key',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const submitBody = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(submitBody).toEqual({
|
||||
model: 'cogview-4',
|
||||
prompt: 'A cute cat on window',
|
||||
quality: 'standard',
|
||||
size: '1280x1280',
|
||||
watermark_enabled: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ imageUrl: mockImageUrl });
|
||||
});
|
||||
|
||||
it('should convert width and height to size parameter', async () => {
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ id: 'zhipu-task-456' }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
task_status: 'SUCCESS',
|
||||
image_result: [{ url: 'https://cdn.zhipu.ai/size.png' }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'glm-image',
|
||||
params: {
|
||||
prompt: 'Landscape',
|
||||
height: 768,
|
||||
width: 1024,
|
||||
},
|
||||
};
|
||||
|
||||
await createZhipuImage(payload, mockOptions);
|
||||
|
||||
const submitBody = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(submitBody).toEqual({
|
||||
model: 'glm-image',
|
||||
prompt: 'Landscape',
|
||||
size: '1024x768',
|
||||
watermark_enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect explicit size and watermark parameter', async () => {
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ id: 'zhipu-task-789' }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
task_status: 'SUCCESS',
|
||||
image_result: [{ url: 'https://cdn.zhipu.ai/watermark.png' }],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'glm-image',
|
||||
params: {
|
||||
prompt: 'Poster',
|
||||
size: '1024x1024',
|
||||
watermark: true,
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
};
|
||||
|
||||
await createZhipuImage(payload, mockOptions);
|
||||
|
||||
const submitBody = JSON.parse((fetch as any).mock.calls[0][1].body);
|
||||
expect(submitBody).toEqual({
|
||||
model: 'glm-image',
|
||||
prompt: 'Poster',
|
||||
size: '1024x1024',
|
||||
watermark_enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw on HTTP error', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 402,
|
||||
text: async () => 'Insufficient credits',
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'glm-image',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
await expect(createZhipuImage(payload, mockOptions)).rejects.toThrow(
|
||||
'Zhipu image API error: 402 Insufficient credits',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when response is missing id', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'glm-image',
|
||||
params: { prompt: 'Test' },
|
||||
};
|
||||
|
||||
await expect(createZhipuImage(payload, mockOptions)).rejects.toThrow(
|
||||
'Invalid response: missing task id',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when sync response is missing image url', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
created: 123,
|
||||
data: [],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload: CreateImagePayload = {
|
||||
model: 'cogview-4',
|
||||
params: { prompt: 'Test sync missing url' },
|
||||
};
|
||||
|
||||
await expect(createZhipuImage(payload, mockOptions)).rejects.toThrow(
|
||||
'Invalid sync response: missing image URL',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pollZhipuImageStatus', () => {
|
||||
const options = {
|
||||
apiKey: 'test-api-key',
|
||||
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
};
|
||||
|
||||
it('should return success when task succeeded', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
task_status: 'SUCCESS',
|
||||
image_result: [{ url: 'https://cdn.zhipu.ai/success.png' }],
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await pollZhipuImageStatus('task-123', options);
|
||||
|
||||
expect(result).toEqual({
|
||||
imageUrl: 'https://cdn.zhipu.ai/success.png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when task failed', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
task_status: 'FAIL',
|
||||
error: { message: 'Content moderation failed' },
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(pollZhipuImageStatus('task-123', options)).rejects.toThrow(
|
||||
'Content moderation failed',
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep polling when task is pending then succeed', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ task_status: 'RUNNING' }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
task_status: 'SUCCESS',
|
||||
image_result: [{ url: 'https://cdn.zhipu.ai/polled-success.png' }],
|
||||
}),
|
||||
});
|
||||
|
||||
const resultPromise = pollZhipuImageStatus('task-123', options);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
const result = await resultPromise;
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
expect(result).toEqual({ imageUrl: 'https://cdn.zhipu.ai/polled-success.png' });
|
||||
});
|
||||
|
||||
it('should throw when task succeeded but no image url', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
task_status: 'SUCCESS',
|
||||
image_result: [],
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(pollZhipuImageStatus('task-123', options)).rejects.toThrow(
|
||||
'Task succeeded but no image URL found',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryZhipuImageStatus', () => {
|
||||
it('should query status endpoint correctly', async () => {
|
||||
const mockResponse = {
|
||||
task_status: 'SUCCESS',
|
||||
id: 'task-123',
|
||||
request_id: 'req-456',
|
||||
};
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockResponse,
|
||||
});
|
||||
|
||||
const result = await queryZhipuImageStatus('task-123', {
|
||||
apiKey: 'test-api-key',
|
||||
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
'https://open.bigmodel.cn/api/paas/v4/async-result/task-123',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-api-key',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import createDebug from 'debug';
|
||||
|
||||
import type { CreateImageOptions } from '../../core/openaiCompatibleFactory';
|
||||
import type { CreateImagePayload, CreateImageResponse } from '../../types/image';
|
||||
import type { TaskResult } from '../../utils/asyncifyPolling';
|
||||
import { asyncifyPolling } from '../../utils/asyncifyPolling';
|
||||
|
||||
const log = createDebug('lobe-image:zhipu');
|
||||
|
||||
interface ZhipuImageStatusResponse {
|
||||
created?: number;
|
||||
data?: Array<{
|
||||
url?: string;
|
||||
}>;
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
id?: string;
|
||||
image_result?: Array<{
|
||||
b64_json?: string;
|
||||
url?: string;
|
||||
}>;
|
||||
request_id?: string;
|
||||
task_status?: string;
|
||||
}
|
||||
|
||||
export async function queryZhipuImageStatus(
|
||||
inferenceId: string,
|
||||
options: { apiKey: string; baseURL: string },
|
||||
): Promise<ZhipuImageStatusResponse> {
|
||||
const statusUrl = `${options.baseURL}/async-result/${inferenceId}`;
|
||||
|
||||
log('Querying image status for: %s', inferenceId);
|
||||
|
||||
const response = await fetch(statusUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${options.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Zhipu image status API error: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ZhipuImageStatusResponse;
|
||||
log('Image status response: %O', data);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function pollZhipuImageStatus(
|
||||
inferenceId: string,
|
||||
options: { apiKey: string; baseURL: string },
|
||||
): Promise<CreateImageResponse> {
|
||||
return await asyncifyPolling<ZhipuImageStatusResponse, CreateImageResponse>({
|
||||
backoffMultiplier: 1,
|
||||
checkStatus: (taskStatus): TaskResult<CreateImageResponse> => {
|
||||
if (taskStatus.task_status === 'SUCCESS') {
|
||||
const imageUrl = taskStatus.image_result?.[0]?.url;
|
||||
|
||||
if (!imageUrl) {
|
||||
return {
|
||||
error: new Error('Task succeeded but no image URL found'),
|
||||
status: 'failed',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: { imageUrl },
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
if (taskStatus.task_status === 'FAIL') {
|
||||
return {
|
||||
error: new Error(taskStatus.error?.message || 'Image generation failed'),
|
||||
status: 'failed',
|
||||
};
|
||||
}
|
||||
|
||||
return { status: 'pending' };
|
||||
},
|
||||
logger: {
|
||||
debug: (message: any, ...args: any[]) => log(message, ...args),
|
||||
error: (message: any, ...args: any[]) => log(message, ...args),
|
||||
},
|
||||
pollingQuery: () => queryZhipuImageStatus(inferenceId, options),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Zhipu image generation implementation
|
||||
* API docs: https://open.bigmodel.cn
|
||||
*/
|
||||
export async function createZhipuImage(
|
||||
payload: CreateImagePayload,
|
||||
options: CreateImageOptions,
|
||||
): Promise<CreateImageResponse> {
|
||||
const { model, params } = payload;
|
||||
const { prompt, resolution, size, watermark, width, height } = params;
|
||||
|
||||
log('Creating image with Zhipu API - model: %s, params: %O', model, params);
|
||||
|
||||
const baseURL = options.baseURL || 'https://open.bigmodel.cn/api/paas/v4';
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model,
|
||||
prompt,
|
||||
...(resolution && { quality: resolution }),
|
||||
};
|
||||
|
||||
if (size) {
|
||||
body.size = size;
|
||||
} else if (width !== undefined && height !== undefined) {
|
||||
body.size = `${width}x${height}`;
|
||||
}
|
||||
|
||||
body.watermark_enabled = watermark ?? false;
|
||||
|
||||
const isSyncModel = model.startsWith('cogview');
|
||||
const endpoint = isSyncModel
|
||||
? `${baseURL}/images/generations`
|
||||
: `${baseURL}/async/images/generations`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Authorization': `Bearer ${options.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
log('Zhipu image API error: %s %s', response.status, errorText);
|
||||
throw new Error(`Zhipu image API error: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as ZhipuImageStatusResponse;
|
||||
log('Zhipu image API response: %O', data);
|
||||
|
||||
const imageUrl = data.data?.[0]?.url;
|
||||
if (imageUrl) {
|
||||
return { imageUrl };
|
||||
}
|
||||
|
||||
if (isSyncModel) {
|
||||
throw new Error('Invalid sync response: missing image URL');
|
||||
}
|
||||
|
||||
if (!data.id) {
|
||||
throw new Error('Invalid response: missing task id');
|
||||
}
|
||||
|
||||
return await pollZhipuImageStatus(data.id, {
|
||||
apiKey: options.apiKey,
|
||||
baseURL,
|
||||
});
|
||||
}
|
||||
@@ -90,7 +90,18 @@ export async function createZhipuVideo(
|
||||
options: CreateVideoOptions,
|
||||
): Promise<CreateVideoResponse> {
|
||||
const { model, params } = payload;
|
||||
const { prompt, imageUrl, endImageUrl, aspectRatio, duration, generateAudio, size } = params;
|
||||
const {
|
||||
prompt,
|
||||
imageUrl,
|
||||
imageUrls,
|
||||
endImageUrl,
|
||||
aspectRatio,
|
||||
duration,
|
||||
generateAudio,
|
||||
resolution,
|
||||
size,
|
||||
watermark,
|
||||
} = params;
|
||||
|
||||
log('Creating video with Zhipu API - model: %s, params: %O', model, params);
|
||||
|
||||
@@ -104,15 +115,18 @@ export async function createZhipuVideo(
|
||||
|
||||
// Zhipu requires image_url as an array: [first_frame, last_frame?]
|
||||
// https://docs.bigmodel.cn/cn/guide/paid-recommendation/cogvideox
|
||||
const imageUrls: string[] = [];
|
||||
const content: string[] = [];
|
||||
if (imageUrl) {
|
||||
imageUrls.push(imageUrl);
|
||||
content.push(imageUrl);
|
||||
}
|
||||
if (imageUrls && imageUrls.length > 0) {
|
||||
imageUrls.forEach((url) => content.push(url));
|
||||
}
|
||||
if (endImageUrl) {
|
||||
imageUrls.push(endImageUrl);
|
||||
content.push(endImageUrl);
|
||||
}
|
||||
if (imageUrls.length > 0) {
|
||||
body.image_url = imageUrls;
|
||||
if (content.length > 0) {
|
||||
body.image_url = content;
|
||||
}
|
||||
|
||||
// Add other optional parameters
|
||||
@@ -120,6 +134,8 @@ export async function createZhipuVideo(
|
||||
if (duration) body.duration = duration;
|
||||
if (generateAudio !== undefined) body.with_audio = generateAudio;
|
||||
if (size) body.size = size;
|
||||
if (resolution) body.quality = resolution;
|
||||
if (watermark !== undefined) body.watermark_enabled = watermark;
|
||||
|
||||
log('Zhipu video API request body: %O', body);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { OpenAIStream } from '../../core/streams/openai';
|
||||
import { convertIterableToStream } from '../../core/streams/protocol';
|
||||
import { getModelMaxOutputs } from '../../utils/getModelMaxOutputs';
|
||||
import { MODEL_LIST_CONFIGS, processModelList } from '../../utils/modelParse';
|
||||
import { createZhipuImage } from './createImage';
|
||||
import { createZhipuVideo } from './createVideo';
|
||||
|
||||
export interface ZhipuModelCard {
|
||||
@@ -142,6 +143,7 @@ export const params = {
|
||||
});
|
||||
},
|
||||
},
|
||||
createImage: createZhipuImage,
|
||||
createVideo: createZhipuVideo,
|
||||
handlePollVideoStatus: async (inferenceId, options) => {
|
||||
const { pollZhipuVideoStatus } = await import('./createVideo');
|
||||
|
||||
@@ -10,6 +10,7 @@ export default {
|
||||
'config.imageUrl.label': 'Reference Image',
|
||||
'config.imageUrls.label': 'Reference Images',
|
||||
'config.model.label': 'Model',
|
||||
'config.promptExtend.label': 'Prompt Extend',
|
||||
'config.prompt.placeholder': 'Describe what you want to generate',
|
||||
'config.prompt.placeholderWithRef': 'Describe how you want to adjust the image',
|
||||
'config.quality.label': 'Image Quality',
|
||||
@@ -24,7 +25,9 @@ export default {
|
||||
'config.size.label': 'Size',
|
||||
'config.steps.label': 'Steps',
|
||||
'config.title': 'Configuration',
|
||||
'config.watermark.label': 'Watermark',
|
||||
'config.width.label': 'Width',
|
||||
'config.webSearch.label': 'Web Search',
|
||||
'generation.actions.applySeed': 'Apply Seed',
|
||||
'generation.actions.copyError': 'Copy Error Message',
|
||||
'generation.actions.copyPrompt': 'Copy Prompt',
|
||||
|
||||
@@ -6,6 +6,7 @@ export default {
|
||||
'config.generateAudio.label': 'Generate Audio',
|
||||
'config.header.title': 'Video',
|
||||
'config.imageUrl.label': 'Start Frame',
|
||||
'config.promptExtend.label': 'Prompt Extend',
|
||||
'config.prompt.placeholder': 'Describe the video you want to generate',
|
||||
'config.prompt.placeholderWithRef': 'Describe the scene you want to generate with the image',
|
||||
'config.referenceImage.label': 'Reference Image',
|
||||
@@ -13,6 +14,8 @@ export default {
|
||||
'config.seed.label': 'Seed',
|
||||
'config.seed.random': 'Random',
|
||||
'config.size.label': 'Size',
|
||||
'config.watermark.label': 'Watermark',
|
||||
'config.webSearch.label': 'Web Search',
|
||||
'generation.actions.copyError': 'Copy Error Message',
|
||||
'generation.actions.errorCopied': 'Error Message Copied to Clipboard',
|
||||
'generation.actions.errorCopyFailed': 'Failed to Copy Error Message',
|
||||
|
||||
@@ -3,16 +3,34 @@
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { createStaticStyles, cssVar } from 'antd-style';
|
||||
import { ArrowLeftRight } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import UploadCard, { UPLOAD_CARD_SIZE, type UploadData } from './UploadCard';
|
||||
|
||||
const STACK_OFFSET = -(UPLOAD_CARD_SIZE - 8);
|
||||
const EXPAND_OFFSET = 4;
|
||||
|
||||
const styles = createStaticStyles(({ css }) => ({
|
||||
addCirclePos: css`
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
inset-block-end: -2px;
|
||||
inset-inline-end: -2px;
|
||||
`,
|
||||
refGroup: css`
|
||||
position: relative;
|
||||
`,
|
||||
stack: css`
|
||||
position: relative;
|
||||
padding-block: 4px;
|
||||
padding-inline: 0;
|
||||
|
||||
&:hover {
|
||||
.inline-ref-close {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
swapIcon: css`
|
||||
flex-shrink: 0;
|
||||
@@ -23,28 +41,134 @@ const styles = createStaticStyles(({ css }) => ({
|
||||
interface InlineVideoFramesProps {
|
||||
endImageUrl?: string | null;
|
||||
imageUrl?: string | null;
|
||||
imageUrls?: string[] | null;
|
||||
isSupportEndImage?: boolean;
|
||||
maxCount?: number;
|
||||
maxFileSize?: number;
|
||||
onEndImageChange: (data: UploadData | null) => void;
|
||||
onImageChange: (data: UploadData | null) => void;
|
||||
onImageUrlsChange?: (data: UploadData) => void;
|
||||
onRemoveImageUrl?: (url: string) => void;
|
||||
}
|
||||
|
||||
const InlineVideoFrames = memo<InlineVideoFramesProps>(
|
||||
({ imageUrl, endImageUrl, onImageChange, onEndImageChange, isSupportEndImage = true }) => {
|
||||
({
|
||||
imageUrl,
|
||||
imageUrls,
|
||||
endImageUrl,
|
||||
onImageChange,
|
||||
onEndImageChange,
|
||||
onImageUrlsChange,
|
||||
onRemoveImageUrl,
|
||||
isSupportEndImage = true,
|
||||
maxCount = 5,
|
||||
maxFileSize,
|
||||
}) => {
|
||||
const { t } = useTranslation('video');
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const hasStartFrame = Boolean(imageUrl);
|
||||
const showEndFrame = isSupportEndImage && hasStartFrame;
|
||||
// Combine imageUrl and imageUrls for display
|
||||
const refFrameUrls = useMemo(() => {
|
||||
const urls: string[] = [];
|
||||
if (imageUrl) urls.push(imageUrl);
|
||||
if (Array.isArray(imageUrls)) {
|
||||
urls.push(...imageUrls);
|
||||
}
|
||||
return urls;
|
||||
}, [imageUrl, imageUrls]);
|
||||
|
||||
const hasRefFrames = refFrameUrls.length > 0;
|
||||
const canAddMore = refFrameUrls.length < maxCount;
|
||||
const shouldCollapse = hasRefFrames && !isHovered;
|
||||
const showEndFrame = isSupportEndImage && hasRefFrames;
|
||||
|
||||
return (
|
||||
<Flexbox horizontal align={'end'} className={styles.stack} gap={6}>
|
||||
<UploadCard
|
||||
imageUrl={imageUrl}
|
||||
label={hasStartFrame ? t('config.imageUrl.label') : t('config.referenceImage.label')}
|
||||
onRemove={() => onImageChange(null)}
|
||||
onUpload={(data) => onImageChange(data)}
|
||||
/>
|
||||
<Flexbox
|
||||
horizontal
|
||||
align={'end'}
|
||||
className={styles.refGroup}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Render ref frames (from imageUrl and imageUrls) */}
|
||||
{refFrameUrls.map((url, index) => {
|
||||
const isFromImageUrl = url === imageUrl;
|
||||
const label =
|
||||
index === 0 && isFromImageUrl
|
||||
? t('config.imageUrl.label')
|
||||
: t('config.referenceImage.label');
|
||||
|
||||
return (
|
||||
<UploadCard
|
||||
closeClassName="inline-ref-close"
|
||||
imageUrl={url}
|
||||
key={url}
|
||||
label={label}
|
||||
maxFileSize={maxFileSize}
|
||||
style={{
|
||||
marginInlineStart:
|
||||
index > 0 ? (shouldCollapse ? STACK_OFFSET : EXPAND_OFFSET) : 0,
|
||||
zIndex: index + 1,
|
||||
}}
|
||||
onRemove={() => {
|
||||
if (isFromImageUrl && imageUrl === url) {
|
||||
onImageChange(null);
|
||||
} else if (onRemoveImageUrl) {
|
||||
onRemoveImageUrl(url);
|
||||
}
|
||||
}}
|
||||
onUpload={
|
||||
isFromImageUrl
|
||||
? (data) => onImageChange(data)
|
||||
: (data) => onImageUrlsChange?.(data)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Add new frame button */}
|
||||
{canAddMore &&
|
||||
(shouldCollapse ? (
|
||||
<UploadCard
|
||||
className={styles.addCirclePos}
|
||||
maxFileSize={maxFileSize}
|
||||
variant="circle"
|
||||
onRemove={() => {}}
|
||||
onUpload={(data) => {
|
||||
if (onImageUrlsChange) {
|
||||
onImageUrlsChange(data);
|
||||
} else {
|
||||
onImageChange(data);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UploadCard
|
||||
imageUrl={null}
|
||||
label={t('config.referenceImage.label')}
|
||||
maxFileSize={maxFileSize}
|
||||
style={{
|
||||
marginInlineStart: hasRefFrames ? EXPAND_OFFSET : 0,
|
||||
zIndex: refFrameUrls.length + 1,
|
||||
}}
|
||||
onRemove={() => {}}
|
||||
onUpload={(data) => {
|
||||
if (hasRefFrames) {
|
||||
if (onImageUrlsChange) {
|
||||
onImageUrlsChange(data);
|
||||
} else {
|
||||
onImageChange(data);
|
||||
}
|
||||
} else {
|
||||
onImageChange(data);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Flexbox>
|
||||
|
||||
{/* End frame separator and upload */}
|
||||
{showEndFrame && (
|
||||
<>
|
||||
<Flexbox
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { ModelIcon } from '@lobehub/icons';
|
||||
import { ActionIcon, Flexbox, Text } from '@lobehub/ui';
|
||||
import { ActionIcon, Flexbox, Segmented, Text } from '@lobehub/ui';
|
||||
import { Divider, Switch } from 'antd';
|
||||
import { Images } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { loginRequired } from '@/components/Error/loginRequiredNotification';
|
||||
@@ -49,6 +50,52 @@ interface PromptInputProps {
|
||||
|
||||
const isSupportedParamSelector = imageGenerationConfigSelectors.isSupportedParam;
|
||||
|
||||
interface SwitchItemProps {
|
||||
label: string;
|
||||
paramName: 'watermark' | 'webSearch';
|
||||
}
|
||||
|
||||
const SwitchItem = memo<SwitchItemProps>(({ label, paramName }) => {
|
||||
const { value, setValue } = useGenerationConfigParam(paramName);
|
||||
|
||||
return (
|
||||
<Flexbox horizontal align="center" justify="space-between" padding={'0 2px'}>
|
||||
<Text weight={500}>{label}</Text>
|
||||
<Switch checked={!!value} onChange={(checked) => setValue(checked as any)} />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
const PromptExtendItem = memo(() => {
|
||||
const { t } = useTranslation('image');
|
||||
const { value, setValue, enumValues } = useGenerationConfigParam('promptExtend');
|
||||
|
||||
if (enumValues && enumValues.length > 0) {
|
||||
const options = enumValues.map((item) => ({ label: item, value: item }));
|
||||
|
||||
return (
|
||||
<Flexbox gap={6}>
|
||||
<Text weight={500}>{t('config.promptExtend.label')}</Text>
|
||||
<Segmented
|
||||
block
|
||||
options={options}
|
||||
style={{ width: '100%' }}
|
||||
value={value as string}
|
||||
variant="filled"
|
||||
onChange={(next) => setValue(String(next) as any)}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flexbox horizontal align="center" justify="space-between" padding={'0 2px'}>
|
||||
<Text weight={500}>{t('config.promptExtend.label')}</Text>
|
||||
<Switch checked={!!value} onChange={(checked) => setValue(checked as any)} />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
const isDarkMode = useIsDark();
|
||||
const { t } = useTranslation('image');
|
||||
@@ -75,6 +122,9 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
const isSupportSeed = useImageStore(isSupportedParamSelector('seed'));
|
||||
const isSupportSteps = useImageStore(isSupportedParamSelector('steps'));
|
||||
const isSupportCfg = useImageStore(isSupportedParamSelector('cfg'));
|
||||
const isSupportPromptExtend = useImageStore(isSupportedParamSelector('promptExtend'));
|
||||
const isSupportWatermark = useImageStore(isSupportedParamSelector('watermark'));
|
||||
const isSupportWebSearch = useImageStore(isSupportedParamSelector('webSearch'));
|
||||
const isLogin = useUserStore(authSelectors.isLogin);
|
||||
const enabledImageModelList = useAiInfraStore(aiProviderSelectors.enabledImageModelList);
|
||||
const { showDimensionControl } = useDimensionControl();
|
||||
@@ -120,9 +170,13 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
hasProcessedPrompt.current = true;
|
||||
setPromptParam(null);
|
||||
|
||||
setTimeout(async () => {
|
||||
const timeoutId = window.setTimeout(async () => {
|
||||
await createImage();
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
}, [promptParam, isLogin, setValue, setPromptParam, createImage]);
|
||||
|
||||
@@ -266,6 +320,16 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
<SeedNumberInput />
|
||||
</Flexbox>
|
||||
)}
|
||||
{(isSupportWatermark || isSupportPromptExtend || isSupportWebSearch) && (
|
||||
<Divider style={{ marginBlock: 4 }} />
|
||||
)}
|
||||
{isSupportWatermark && (
|
||||
<SwitchItem label={t('config.watermark.label')} paramName={'watermark'} />
|
||||
)}
|
||||
{isSupportPromptExtend && <PromptExtendItem />}
|
||||
{isSupportWebSearch && (
|
||||
<SwitchItem label={t('config.webSearch.label')} paramName={'webSearch'} />
|
||||
)}
|
||||
</Flexbox>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -214,7 +214,10 @@ export const VideoGenerationBatchItem = memo<VideoGenerationBatchItemProps>(({ b
|
||||
);
|
||||
};
|
||||
|
||||
const hasReferenceFrames = batch.config?.imageUrl || batch.config?.endImageUrl;
|
||||
const hasReferenceFrames =
|
||||
batch.config?.imageUrl ||
|
||||
(batch.config?.imageUrls && batch.config.imageUrls.length > 0) ||
|
||||
batch.config?.endImageUrl;
|
||||
|
||||
return (
|
||||
<Block className={styles.container} gap={8} variant={'borderless'}>
|
||||
@@ -223,6 +226,7 @@ export const VideoGenerationBatchItem = memo<VideoGenerationBatchItemProps>(({ b
|
||||
<VideoReferenceFrames
|
||||
endImageUrl={batch.config?.endImageUrl}
|
||||
imageUrl={batch.config?.imageUrl}
|
||||
imageUrls={batch.config?.imageUrls}
|
||||
/>
|
||||
)}
|
||||
<Markdown variant={'chat'}>{batch.prompt}</Markdown>
|
||||
|
||||
@@ -37,42 +37,46 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
interface VideoReferenceFramesProps {
|
||||
endImageUrl?: string | null;
|
||||
imageUrl?: string | null;
|
||||
imageUrls?: string[];
|
||||
}
|
||||
|
||||
const VideoReferenceFrames = memo<VideoReferenceFramesProps>(({ imageUrl, endImageUrl }) => {
|
||||
const allImages: string[] = [];
|
||||
if (imageUrl) allImages.push(imageUrl);
|
||||
if (endImageUrl) allImages.push(endImageUrl);
|
||||
const VideoReferenceFrames = memo<VideoReferenceFramesProps>(
|
||||
({ imageUrl, imageUrls, endImageUrl }) => {
|
||||
const allImages: string[] = [];
|
||||
if (imageUrl) allImages.push(imageUrl);
|
||||
if (imageUrls && imageUrls.length > 0) allImages.push(...imageUrls);
|
||||
if (endImageUrl) allImages.push(endImageUrl);
|
||||
|
||||
if (allImages.length === 0) return null;
|
||||
if (allImages.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Image.PreviewGroup>
|
||||
<Flexbox horizontal align={'flex-end'} flex={'none'} wrap="wrap">
|
||||
<ActionIcon
|
||||
glass
|
||||
className={styles.icon}
|
||||
icon={QuoteIcon}
|
||||
size={'small'}
|
||||
variant={'filled'}
|
||||
/>
|
||||
{allImages.map((url, index) => (
|
||||
<div className={styles.container} key={`${url}-${index}`}>
|
||||
<Image
|
||||
alt={index === 0 ? 'Start frame' : 'End frame'}
|
||||
className={styles.image}
|
||||
height={'100%'}
|
||||
src={url}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
variant={'outlined'}
|
||||
width={'100%'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Flexbox>
|
||||
</Image.PreviewGroup>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Image.PreviewGroup>
|
||||
<Flexbox horizontal align={'flex-end'} flex={'none'} wrap="wrap">
|
||||
<ActionIcon
|
||||
glass
|
||||
className={styles.icon}
|
||||
icon={QuoteIcon}
|
||||
size={'small'}
|
||||
variant={'filled'}
|
||||
/>
|
||||
{allImages.map((url, index) => (
|
||||
<div className={styles.container} key={`${url}-${index}`}>
|
||||
<Image
|
||||
alt={index === 0 ? 'Start frame' : 'End frame'}
|
||||
className={styles.image}
|
||||
height={'100%'}
|
||||
src={url}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
variant={'outlined'}
|
||||
width={'100%'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Flexbox>
|
||||
</Image.PreviewGroup>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
VideoReferenceFrames.displayName = 'VideoReferenceFrames';
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ const SeedItem = memo(() => {
|
||||
|
||||
interface SwitchItemProps {
|
||||
label: string;
|
||||
paramName: 'cameraFixed' | 'generateAudio';
|
||||
paramName: 'cameraFixed' | 'generateAudio' | 'watermark' | 'webSearch';
|
||||
}
|
||||
|
||||
const SwitchItem = memo<SwitchItemProps>(({ label, paramName }) => {
|
||||
@@ -167,11 +167,48 @@ const SwitchItem = memo<SwitchItemProps>(({ label, paramName }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const PromptExtendItem = memo(() => {
|
||||
const { t } = useTranslation('video');
|
||||
const { value, setValue, enumValues } = useVideoGenerationConfigParam('promptExtend');
|
||||
|
||||
const options = enumValues?.map((item) => ({ label: item, value: item })) ?? [];
|
||||
|
||||
if (options.length > 0) {
|
||||
return (
|
||||
<Flexbox gap={6}>
|
||||
<Text weight={500}>{t('config.promptExtend.label')}</Text>
|
||||
<Segmented
|
||||
block
|
||||
options={options}
|
||||
style={{ width: '100%' }}
|
||||
value={value as string}
|
||||
variant="filled"
|
||||
onChange={(next) => setValue(String(next) as any)}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flexbox horizontal align="center" justify="space-between" padding={'0 2px'}>
|
||||
<Text weight={500}>{t('config.promptExtend.label')}</Text>
|
||||
<Switch checked={!!value} onChange={(checked) => setValue(checked as any)} />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
const isDarkMode = useIsDark();
|
||||
const { t } = useTranslation('video');
|
||||
const { value, setValue } = useVideoGenerationConfigParam('prompt');
|
||||
const { value: imageUrl, setValue: setImageUrl } = useVideoGenerationConfigParam('imageUrl');
|
||||
const {
|
||||
value: imageUrls,
|
||||
setValue: setImageUrls,
|
||||
maxCount: imageUrlsMaxCount,
|
||||
maxFileSize: imageUrlsMaxFileSize,
|
||||
} = useVideoGenerationConfigParam('imageUrls');
|
||||
const { maxFileSize: imageUrlMaxFileSize } = useVideoGenerationConfigParam('imageUrl');
|
||||
const { value: endImageUrl, setValue: setEndImageUrl } =
|
||||
useVideoGenerationConfigParam('endImageUrl');
|
||||
const isCreating = useVideoStore(createVideoSelectors.isCreating);
|
||||
@@ -182,6 +219,7 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
const enabledVideoModelList = useAiInfraStore(aiProviderSelectors.enabledVideoModelList);
|
||||
const isInit = useVideoStore((s) => s.isInit);
|
||||
const isSupportImageUrl = useVideoStore(isSupportedParamSelector('imageUrl'));
|
||||
const isSupportImageUrls = useVideoStore(isSupportedParamSelector('imageUrls'));
|
||||
const isSupportEndImageUrl = useVideoStore(isSupportedParamSelector('endImageUrl'));
|
||||
const isSupportAspectRatio = useVideoStore(isSupportedParamSelector('aspectRatio'));
|
||||
const isSupportResolution = useVideoStore(isSupportedParamSelector('resolution'));
|
||||
@@ -189,7 +227,10 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
const isSupportDuration = useVideoStore(isSupportedParamSelector('duration'));
|
||||
const isSupportSeed = useVideoStore(isSupportedParamSelector('seed'));
|
||||
const isSupportGenerateAudio = useVideoStore(isSupportedParamSelector('generateAudio'));
|
||||
const isSupportPromptExtend = useVideoStore(isSupportedParamSelector('promptExtend'));
|
||||
const isSupportWatermark = useVideoStore(isSupportedParamSelector('watermark'));
|
||||
const isSupportCameraFixed = useVideoStore(isSupportedParamSelector('cameraFixed'));
|
||||
const isSupportWebSearch = useVideoStore(isSupportedParamSelector('webSearch'));
|
||||
const isLogin = useUserStore(authSelectors.isLogin);
|
||||
const { value: duration } = useVideoGenerationConfigParam('duration');
|
||||
useFetchAiVideoConfig();
|
||||
@@ -237,25 +278,64 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
|
||||
setPromptParam(null);
|
||||
|
||||
setTimeout(async () => {
|
||||
const timeoutId = window.setTimeout(async () => {
|
||||
await createVideo();
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
}, [promptParam, isLogin, setValue, setPromptParam, createVideo]);
|
||||
|
||||
const showInlineFrames = isSupportImageUrl || isSupportEndImageUrl;
|
||||
const hasRefImages = Boolean(imageUrl || endImageUrl);
|
||||
const showInlineFrames = isSupportImageUrl || isSupportImageUrls || isSupportEndImageUrl;
|
||||
const framePreviewUrls = useMemo(
|
||||
() => [imageUrl, ...(imageUrls ?? [])].filter(Boolean) as string[],
|
||||
[imageUrl, imageUrls],
|
||||
);
|
||||
const hasRefImages = framePreviewUrls.length > 0 || Boolean(endImageUrl);
|
||||
const maxCount = useMemo(() => {
|
||||
let count = 0;
|
||||
if (isSupportImageUrl) count += 1;
|
||||
if (isSupportImageUrls) count += imageUrlsMaxCount ?? 4;
|
||||
return count;
|
||||
}, [isSupportImageUrl, isSupportImageUrls, imageUrlsMaxCount]);
|
||||
|
||||
const handleImageChange = useCallback(
|
||||
(data: string | { dimensions?: { height: number; width: number }; url: string } | null) => {
|
||||
if (data === null) {
|
||||
setImageUrl(null as any);
|
||||
return;
|
||||
}
|
||||
const handleAddImage = useCallback(
|
||||
(data: string | { dimensions?: { height: number; width: number }; url: string }) => {
|
||||
const url = typeof data === 'string' ? data : data?.url;
|
||||
setImageUrl((url ?? null) as any);
|
||||
if (!url) return;
|
||||
if (framePreviewUrls.length >= maxCount) return;
|
||||
|
||||
if (isSupportImageUrl && !imageUrl) {
|
||||
setImageUrl(url);
|
||||
} else if (isSupportImageUrls) {
|
||||
setImageUrls([...(imageUrls ?? []), url] as any);
|
||||
} else if (isSupportImageUrl) {
|
||||
setImageUrl(url);
|
||||
}
|
||||
},
|
||||
[setImageUrl],
|
||||
[
|
||||
isSupportImageUrl,
|
||||
isSupportImageUrls,
|
||||
imageUrl,
|
||||
imageUrls,
|
||||
setImageUrl,
|
||||
setImageUrls,
|
||||
framePreviewUrls.length,
|
||||
maxCount,
|
||||
],
|
||||
);
|
||||
|
||||
const handleRemoveImage = useCallback(
|
||||
(url: string) => {
|
||||
if (url === imageUrl) {
|
||||
setImageUrl(null);
|
||||
} else {
|
||||
setImageUrls((imageUrls ?? []).filter((item) => item !== url) as any);
|
||||
}
|
||||
},
|
||||
[imageUrl, imageUrls, setImageUrl, setImageUrls],
|
||||
);
|
||||
|
||||
const handleEndImageChange = useCallback(
|
||||
@@ -286,9 +366,20 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
<InlineVideoFrames
|
||||
endImageUrl={endImageUrl}
|
||||
imageUrl={imageUrl}
|
||||
imageUrls={imageUrls}
|
||||
isSupportEndImage={isSupportEndImageUrl}
|
||||
maxCount={maxCount}
|
||||
maxFileSize={imageUrlsMaxFileSize ?? imageUrlMaxFileSize}
|
||||
onEndImageChange={handleEndImageChange}
|
||||
onImageChange={handleImageChange}
|
||||
onImageUrlsChange={handleAddImage}
|
||||
onRemoveImageUrl={handleRemoveImage}
|
||||
onImageChange={(data) => {
|
||||
if (data === null) {
|
||||
handleRemoveImage(imageUrl || '');
|
||||
return;
|
||||
}
|
||||
handleAddImage(data);
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
@@ -343,9 +434,11 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
<SeedItem />
|
||||
</Flexbox>
|
||||
)}
|
||||
{(isSupportGenerateAudio || isSupportCameraFixed) && (
|
||||
<Divider style={{ marginBlock: 4 }} />
|
||||
)}
|
||||
{(isSupportGenerateAudio ||
|
||||
isSupportCameraFixed ||
|
||||
isSupportWatermark ||
|
||||
isSupportPromptExtend ||
|
||||
isSupportWebSearch) && <Divider style={{ marginBlock: 4 }} />}
|
||||
{isSupportGenerateAudio && (
|
||||
<SwitchItem
|
||||
label={t('config.generateAudio.label')}
|
||||
@@ -355,6 +448,13 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
||||
{isSupportCameraFixed && (
|
||||
<SwitchItem label={t('config.cameraFixed.label')} paramName={'cameraFixed'} />
|
||||
)}
|
||||
{isSupportWatermark && (
|
||||
<SwitchItem label={t('config.watermark.label')} paramName={'watermark'} />
|
||||
)}
|
||||
{isSupportPromptExtend && <PromptExtendItem />}
|
||||
{isSupportWebSearch && (
|
||||
<SwitchItem label={t('config.webSearch.label')} paramName={'webSearch'} />
|
||||
)}
|
||||
</Flexbox>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,7 @@ vi.mock('@/store/user/slices/settings/selectors', () => ({
|
||||
// Test fixtures
|
||||
const customModelSchema: ModelParamsSchema = {
|
||||
prompt: { default: '' },
|
||||
imageUrls: { default: [] },
|
||||
width: { default: 1024, min: 256, max: 2048, step: 64 },
|
||||
height: { default: 1024, min: 256, max: 2048, step: 64 },
|
||||
steps: { default: 20, min: 1, max: 50 },
|
||||
@@ -44,6 +45,17 @@ const testImageModels: AIImageModelCard[] = [
|
||||
parameters: customModelSchema,
|
||||
releasedAt: '2024-01-01',
|
||||
},
|
||||
{
|
||||
id: 'single-image-model',
|
||||
displayName: 'Single Image Model',
|
||||
type: 'image',
|
||||
parameters: {
|
||||
prompt: { default: '' },
|
||||
imageUrl: { default: '' },
|
||||
steps: { default: 20, min: 1, max: 50 },
|
||||
} as ModelParamsSchema,
|
||||
releasedAt: '2024-01-01',
|
||||
},
|
||||
];
|
||||
|
||||
const mockProviders = [
|
||||
@@ -57,6 +69,11 @@ const mockProviders = [
|
||||
name: 'Custom Provider',
|
||||
children: [testImageModels[1]],
|
||||
},
|
||||
{
|
||||
id: 'single-image-provider',
|
||||
name: 'Single Image Provider',
|
||||
children: [testImageModels[2]],
|
||||
},
|
||||
];
|
||||
|
||||
// Mock external dependencies
|
||||
@@ -178,7 +195,10 @@ describe('GenerationConfigAction', () => {
|
||||
|
||||
expect(result.current.model).toBe('flux/schnell');
|
||||
expect(result.current.provider).toBe('fal');
|
||||
expect(result.current.parameters).toEqual(fluxSchnellDefaultValues);
|
||||
expect(result.current.parameters).toEqual({
|
||||
...fluxSchnellDefaultValues,
|
||||
prompt: 'initial prompt',
|
||||
});
|
||||
expect(result.current.parametersSchema).toEqual(fluxSchnellParamsSchema);
|
||||
});
|
||||
|
||||
@@ -191,26 +211,113 @@ describe('GenerationConfigAction', () => {
|
||||
|
||||
expect(result.current.model).toBe('custom-model');
|
||||
expect(result.current.provider).toBe('custom-provider');
|
||||
expect(result.current.parameters).toEqual(customModelDefaultValues);
|
||||
expect(result.current.parameters).toEqual({
|
||||
...customModelDefaultValues,
|
||||
prompt: 'initial prompt',
|
||||
});
|
||||
expect(result.current.parametersSchema).toEqual(customModelSchema);
|
||||
});
|
||||
|
||||
it('should completely replace parameters when switching models', () => {
|
||||
it('should preserve prompt and image inputs when switching models', () => {
|
||||
const { result } = renderHook(() => useImageStore());
|
||||
|
||||
// Set some custom parameters
|
||||
act(() => {
|
||||
result.current.setParamOnInput('prompt', 'custom prompt');
|
||||
result.current.setParamOnInput('imageUrls', ['custom-image-1.png']);
|
||||
result.current.setParamOnInput('steps', 50);
|
||||
});
|
||||
|
||||
// Switch model
|
||||
act(() => {
|
||||
result.current.setModelAndProviderOnSelect('flux/schnell', 'fal');
|
||||
result.current.setModelAndProviderOnSelect('custom-model', 'custom-provider');
|
||||
});
|
||||
|
||||
expect(result.current.parameters).toEqual(fluxSchnellDefaultValues);
|
||||
expect(result.current.parameters?.prompt).toBe('');
|
||||
expect(result.current.parameters).toEqual({
|
||||
...customModelDefaultValues,
|
||||
prompt: 'custom prompt',
|
||||
imageUrls: ['custom-image-1.png'],
|
||||
});
|
||||
expect(result.current.parameters?.steps).toBe(customModelDefaultValues.steps);
|
||||
});
|
||||
|
||||
it('should convert imageUrls[0] to imageUrl when switching to single-image model', () => {
|
||||
const { result } = renderHook(() => useImageStore());
|
||||
|
||||
// Set up multi-image state with imageUrls
|
||||
act(() => {
|
||||
result.current.setParamOnInput('prompt', 'test prompt');
|
||||
result.current.setParamOnInput('imageUrls', ['image1.png', 'image2.png', 'image3.png']);
|
||||
});
|
||||
|
||||
// Switch to single-image model - should convert imageUrls[0] to imageUrl
|
||||
act(() => {
|
||||
result.current.setModelAndProviderOnSelect('single-image-model', 'single-image-provider');
|
||||
});
|
||||
|
||||
expect(result.current.parameters?.imageUrl).toBe('image1.png');
|
||||
expect(result.current.parameters?.prompt).toBe('test prompt');
|
||||
expect(result.current.parameters?.imageUrls).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should convert imageUrl to imageUrls array when switching to multi-image model', () => {
|
||||
const { result } = renderHook(() => useImageStore());
|
||||
const singleImageSchema: ModelParamsSchema = {
|
||||
prompt: { default: '' },
|
||||
imageUrl: { default: '' },
|
||||
steps: { default: 20, min: 1, max: 50 },
|
||||
};
|
||||
|
||||
// Initialize with single-image model state
|
||||
useImageStore.setState({
|
||||
model: 'single-image-model',
|
||||
provider: 'single-image-provider',
|
||||
parameters: {
|
||||
prompt: 'test prompt',
|
||||
imageUrl: 'reference-image.png',
|
||||
steps: 20,
|
||||
},
|
||||
parametersSchema: singleImageSchema,
|
||||
});
|
||||
|
||||
// Get fresh hook after state update
|
||||
const { result: storeResult } = renderHook(() => useImageStore());
|
||||
|
||||
// Switch to multi-image model - should convert imageUrl to imageUrls array
|
||||
act(() => {
|
||||
storeResult.current.setModelAndProviderOnSelect('custom-model', 'custom-provider');
|
||||
});
|
||||
|
||||
expect(storeResult.current.parameters?.imageUrls).toEqual(['reference-image.png']);
|
||||
expect(storeResult.current.parameters?.prompt).toBe('test prompt');
|
||||
expect(storeResult.current.parameters?.imageUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should migrate imageUrl when target model has empty imageUrls default', () => {
|
||||
const singleImageSchema: ModelParamsSchema = {
|
||||
prompt: { default: '' },
|
||||
imageUrl: { default: '' },
|
||||
};
|
||||
|
||||
useImageStore.setState({
|
||||
model: 'single-image-model',
|
||||
provider: 'single-image-provider',
|
||||
parameters: {
|
||||
prompt: 'keep this prompt',
|
||||
imageUrl: 'from-single-model.png',
|
||||
},
|
||||
parametersSchema: singleImageSchema,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useImageStore());
|
||||
|
||||
// custom-model schema defines imageUrls default as []
|
||||
act(() => {
|
||||
result.current.setModelAndProviderOnSelect('custom-model', 'custom-provider');
|
||||
});
|
||||
|
||||
expect(result.current.parameters?.imageUrls).toEqual(['from-single-model.png']);
|
||||
expect(result.current.parameters?.prompt).toBe('keep this prompt');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ import { useUserStore } from '@/store/user';
|
||||
import { authSelectors } from '@/store/user/selectors';
|
||||
import { settingsSelectors } from '@/store/user/slices/settings/selectors';
|
||||
|
||||
import {
|
||||
normalizeImageInputOnSchemaSwitch,
|
||||
preserveSupportedParams,
|
||||
} from '../../../utils/preserveSupportedParams';
|
||||
import { type ImageStore } from '../../store';
|
||||
import { calculateInitialAspectRatio } from '../../utils/aspectRatio';
|
||||
import { adaptSizeToRatio, parseRatio } from '../../utils/size';
|
||||
@@ -64,6 +68,20 @@ function prepareModelConfigState(model: string, provider: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function preserveImageInputParams(
|
||||
previousParameters: RuntimeImageGenParams,
|
||||
nextDefaultValues: RuntimeImageGenParams,
|
||||
nextSchema: ModelParamsSchema,
|
||||
) {
|
||||
const result = preserveSupportedParams(previousParameters, nextDefaultValues, nextSchema, [
|
||||
'prompt',
|
||||
'imageUrl',
|
||||
'imageUrls',
|
||||
]);
|
||||
|
||||
return normalizeImageInputOnSchemaSwitch(previousParameters, nextSchema, result);
|
||||
}
|
||||
|
||||
type Setter = StoreSetter<ImageStore>;
|
||||
export const createGenerationConfigSlice = (set: Setter, get: () => ImageStore, _api?: unknown) =>
|
||||
new GenerationConfigActionImpl(set, get, _api);
|
||||
@@ -244,16 +262,23 @@ export class GenerationConfigActionImpl {
|
||||
};
|
||||
|
||||
setModelAndProviderOnSelect = (model: string, provider: string): void => {
|
||||
const previousParameters = this.#get().parameters;
|
||||
const { defaultValues, parametersSchema, initialActiveRatio } = prepareModelConfigState(
|
||||
model,
|
||||
provider,
|
||||
);
|
||||
|
||||
const parameters = preserveImageInputParams(
|
||||
previousParameters,
|
||||
defaultValues,
|
||||
parametersSchema,
|
||||
);
|
||||
|
||||
this.#set(
|
||||
{
|
||||
model,
|
||||
provider,
|
||||
parameters: defaultValues,
|
||||
parameters,
|
||||
parametersSchema,
|
||||
isAspectRatioLocked: false,
|
||||
activeAspectRatio: initialActiveRatio,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
export function preserveSupportedParams<
|
||||
TParams extends Record<string, unknown>,
|
||||
TSchema extends Record<string, unknown>,
|
||||
TKey extends keyof TParams & string,
|
||||
>(
|
||||
previousParameters: TParams,
|
||||
nextDefaultValues: TParams,
|
||||
nextSchema: TSchema,
|
||||
keys: readonly TKey[],
|
||||
): TParams {
|
||||
const supportedPreservedEntries = keys.flatMap((key) => {
|
||||
if (!(key in nextSchema)) return [];
|
||||
|
||||
const value = previousParameters[key];
|
||||
if (typeof value === 'undefined') return [];
|
||||
|
||||
return [[key, value] as const];
|
||||
});
|
||||
|
||||
return {
|
||||
...nextDefaultValues,
|
||||
...Object.fromEntries(supportedPreservedEntries),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeImageInputOnSchemaSwitch<
|
||||
TParams extends Record<string, unknown> & {
|
||||
imageUrl?: unknown;
|
||||
imageUrls?: unknown;
|
||||
},
|
||||
TSchema extends Record<string, unknown>,
|
||||
>(previousParameters: TParams, nextSchema: TSchema, preservedResult: TParams): TParams {
|
||||
const result = { ...preservedResult };
|
||||
|
||||
const imageUrl = previousParameters.imageUrl;
|
||||
const imageUrls = previousParameters.imageUrls;
|
||||
const supportsImageUrl = 'imageUrl' in nextSchema;
|
||||
const supportsImageUrls = 'imageUrls' in nextSchema;
|
||||
|
||||
// Multi-image -> Single-image
|
||||
if (
|
||||
Array.isArray(imageUrls) &&
|
||||
imageUrls.length > 0 &&
|
||||
!supportsImageUrls &&
|
||||
supportsImageUrl &&
|
||||
!result.imageUrl
|
||||
) {
|
||||
result.imageUrl = imageUrls[0];
|
||||
}
|
||||
|
||||
// Single-image -> Multi-image
|
||||
if (
|
||||
typeof imageUrl === 'string' &&
|
||||
imageUrl &&
|
||||
supportsImageUrls &&
|
||||
!supportsImageUrl &&
|
||||
!(Array.isArray(result.imageUrls) && result.imageUrls.length > 0)
|
||||
) {
|
||||
result.imageUrls = [imageUrl];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import {
|
||||
type AIVideoModelCard,
|
||||
extractVideoDefaultValues,
|
||||
type RuntimeVideoGenParams,
|
||||
type VideoModelParamsSchema,
|
||||
} from 'model-bank';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useVideoStore } from '@/store/video';
|
||||
|
||||
const modelASchema: VideoModelParamsSchema = {
|
||||
prompt: { default: '' },
|
||||
imageUrl: { default: '' },
|
||||
endImageUrl: { default: '' },
|
||||
duration: { default: 5, min: 1, max: 10 },
|
||||
};
|
||||
|
||||
const modelBSchema: VideoModelParamsSchema = {
|
||||
prompt: { default: '' },
|
||||
imageUrl: { default: '' },
|
||||
endImageUrl: { default: '' },
|
||||
duration: { default: 3, min: 1, max: 10 },
|
||||
};
|
||||
|
||||
const testVideoModels: AIVideoModelCard[] = [
|
||||
{
|
||||
id: 'video-model-a',
|
||||
displayName: 'Video Model A',
|
||||
type: 'video',
|
||||
parameters: modelASchema,
|
||||
releasedAt: '2025-01-01',
|
||||
},
|
||||
{
|
||||
id: 'video-model-b',
|
||||
displayName: 'Video Model B',
|
||||
type: 'video',
|
||||
parameters: modelBSchema,
|
||||
releasedAt: '2025-01-02',
|
||||
},
|
||||
];
|
||||
|
||||
const mockProviders = [
|
||||
{
|
||||
id: 'provider-a',
|
||||
name: 'Provider A',
|
||||
children: [testVideoModels[0]],
|
||||
},
|
||||
{
|
||||
id: 'provider-b',
|
||||
name: 'Provider B',
|
||||
children: [testVideoModels[1]],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mock('@/store/aiInfra', () => ({
|
||||
aiProviderSelectors: {
|
||||
enabledVideoModelList: vi.fn(() => mockProviders),
|
||||
},
|
||||
getAiInfraStoreState: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
const modelBDefaultValues = extractVideoDefaultValues(modelBSchema);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
useVideoStore.setState({
|
||||
isInit: true,
|
||||
model: 'video-model-a',
|
||||
provider: 'provider-a',
|
||||
parametersSchema: modelASchema,
|
||||
parameters: {
|
||||
prompt: 'initial prompt',
|
||||
imageUrl: 'start-frame.png',
|
||||
endImageUrl: 'end-frame.png',
|
||||
duration: 6,
|
||||
} as RuntimeVideoGenParams,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('video generationConfig actions', () => {
|
||||
it('should preserve prompt and frame images when switching model', () => {
|
||||
const { result } = renderHook(() => useVideoStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setParamOnInput('prompt', 'cinematic sunset');
|
||||
result.current.setParamOnInput('imageUrl', 'start-custom.png');
|
||||
result.current.setParamOnInput('endImageUrl', 'end-custom.png');
|
||||
result.current.setParamOnInput('duration', 8);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setModelAndProviderOnSelect('video-model-b', 'provider-b');
|
||||
});
|
||||
|
||||
expect(result.current.parameters).toEqual({
|
||||
...modelBDefaultValues,
|
||||
prompt: 'cinematic sunset',
|
||||
imageUrl: 'start-custom.png',
|
||||
endImageUrl: 'end-custom.png',
|
||||
});
|
||||
expect(result.current.parameters?.duration).toBe(modelBDefaultValues.duration);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
type AIVideoModelCard,
|
||||
extractVideoDefaultValues,
|
||||
type RuntimeVideoGenParams,
|
||||
type RuntimeVideoGenParamsKeys,
|
||||
type RuntimeVideoGenParamsValue,
|
||||
type VideoModelParamsSchema,
|
||||
@@ -12,6 +13,10 @@ import { type StoreSetter } from '@/store/types';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { authSelectors } from '@/store/user/selectors';
|
||||
|
||||
import {
|
||||
normalizeImageInputOnSchemaSwitch,
|
||||
preserveSupportedParams,
|
||||
} from '../../../utils/preserveSupportedParams';
|
||||
import type { VideoStore } from '../../store';
|
||||
|
||||
export function getVideoModelAndDefaults(model: string, provider: string) {
|
||||
@@ -39,17 +44,33 @@ export function getVideoModelAndDefaults(model: string, provider: string) {
|
||||
return { activeModel, defaultValues, parametersSchema };
|
||||
}
|
||||
|
||||
function preserveVideoInputParams(
|
||||
previousParameters: RuntimeVideoGenParams,
|
||||
nextDefaultValues: RuntimeVideoGenParams,
|
||||
nextSchema: VideoModelParamsSchema,
|
||||
) {
|
||||
const result = preserveSupportedParams(previousParameters, nextDefaultValues, nextSchema, [
|
||||
'prompt',
|
||||
'imageUrl',
|
||||
'imageUrls',
|
||||
'endImageUrl',
|
||||
]);
|
||||
|
||||
return normalizeImageInputOnSchemaSwitch(previousParameters, nextSchema, result);
|
||||
}
|
||||
|
||||
type Setter = StoreSetter<VideoStore>;
|
||||
|
||||
export const createGenerationConfigSlice = (set: Setter, get: () => VideoStore, _api?: unknown) =>
|
||||
new GenerationConfigActionImpl(set, get, _api);
|
||||
|
||||
export class GenerationConfigActionImpl {
|
||||
readonly #get: () => VideoStore;
|
||||
readonly #set: Setter;
|
||||
|
||||
constructor(set: Setter, _get: () => VideoStore, _api?: unknown) {
|
||||
void _get;
|
||||
constructor(set: Setter, get: () => VideoStore, _api?: unknown) {
|
||||
void _api;
|
||||
this.#get = get;
|
||||
this.#set = set;
|
||||
}
|
||||
|
||||
@@ -85,12 +106,18 @@ export class GenerationConfigActionImpl {
|
||||
};
|
||||
|
||||
setModelAndProviderOnSelect = (model: string, provider: string): void => {
|
||||
const previousParameters = this.#get().parameters;
|
||||
const { defaultValues, parametersSchema } = getVideoModelAndDefaults(model, provider);
|
||||
const parameters = preserveVideoInputParams(
|
||||
previousParameters,
|
||||
defaultValues,
|
||||
parametersSchema,
|
||||
);
|
||||
|
||||
this.#set(
|
||||
{
|
||||
model,
|
||||
parameters: defaultValues,
|
||||
parameters,
|
||||
parametersSchema,
|
||||
provider,
|
||||
},
|
||||
|
||||
@@ -40,9 +40,10 @@ export function useVideoGenerationConfigParam<
|
||||
const enumValues = 'enum' in paramConfig ? (paramConfig.enum as string[]) : undefined;
|
||||
const min = 'min' in paramConfig ? (paramConfig.min as number) : undefined;
|
||||
const max = 'max' in paramConfig ? (paramConfig.max as number) : undefined;
|
||||
const maxCount = 'maxCount' in paramConfig ? (paramConfig.maxCount as number) : undefined;
|
||||
const step = 'step' in paramConfig ? (paramConfig.step as number) : undefined;
|
||||
|
||||
return { enumValues, imageConstraints, max, maxFileSize, min, step };
|
||||
return { enumValues, imageConstraints, max, maxCount, maxFileSize, min, step };
|
||||
}, [paramConfig]);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user