api/main.py | FastAPI 单文件后端:登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回;同时承载全局 prompt_library 和 asset_library 的磁盘索引、CRUD、删除保护和复制到 job API。轻量创作入口 POST /creative/jobs/image 把上传图片或空白底图写成一个只有 0 号关键帧的 Job,让首页直接复用生图/生视频接口;该接口兼容无 body / JSON 空对象 / 正常 multipart 上传,避免无首帧文生图或文生视频时空 multipart 被 FastAPI 在业务前置解析阶段拒绝;/health 返回 image_options、image_size_options、video_options、video_size_options、video_duration_options 和 video_max_duration_seconds;/frames/{idx}/generate 的 model 字段用于图片模型偏好,size 字段用于图片输出尺寸;/storyboard/video 继续使用 model 字段选择视频别名,并先校验画幅与时长能力边界,然后把 GeneratedVideo 写成 queued 占位并进入进程内视频队列。队列默认 VIDEO_QUEUE_MAX_CONCURRENT=2、VIDEO_QUEUE_MAX_CONCURRENT_PER_USER=1,同一用户连续提交不会占满全局并发;排队任务会回写 queue_position、queue_size、queue_message。旧 AgentRun 一键出片状态机、TK 复刻接口和 POST /creative/copy 继续保留。 |
+ video_model_options() | 视频模型能力出口:如果 seedance、kling、veo3、veo 等业务别名实际都映射到同一个真实模型,会按真实模型去重,只给前端返回一个可用选项;当前生产真实模型为 doubao-seedance-2-0-fast-260128,前端显示为 Seedance 2.0 Fast。后续只有在服务器真的配置了不同可用视频模型时,才应把新的模型重新暴露给画布。 |
api/product_library/skg-products | 内置 SKG 白底产品图库:manifest.json 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,images/ 存 45 张参考图。 |
api/character_library/skg-characters | 内置相似主体形象库:从桌面 5 套策划形象导入,manifest.json 记录运动阳光男、都市型男、优雅白领女、运动辣妹、绅士大叔,每套含 7 张透明骨架参考图和一段 prompt_brief。相似主体生成时优先使用文字 brief 作为创意方向,避免把内置图作为强参考图复制。 |
asset_library/ | 全局素材库目录,和 jobs/ 平级,不写入任何 job state。四类目录为 subjects、products、scenes、videos;每个素材自带 manifest.json 和图片/视频文件,index.json 只是启动扫描重建出来的缓存。库素材选用到 job 时必须复制文件到 jobs/<jobId>/assets 或 storyboard-videos,禁止直接保存 library 引用。 |
@@ -1206,6 +1209,20 @@ ProductRefStateItem {
变更记录
这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。
+
+
+ 2026-05-25 · 媒体模型选择对齐真实后端能力
+ API
+ UI
+ Docs
+
+
+
问题:恢复上游画布后,前端仍可能显示 Nano Banana、Seedream、Kling、Veo 等上游或旧缓存模型,但这些并不是当前 SKG 后端已经验证可用的生成通道,用户一旦选择就会失败或误解模型能力。
+
改动:web/canvas-app/src/config/models.js 将图片模型收口为 auto、gpt-image-2、gemini-3-pro-image-preview,视频模型收口为 seedance / Seedance 2.0 Fast;图片尺寸、视频画幅和视频时长对齐后端真实能力。useModelConfig.js 和 Pinia 模型 store 会忽略浏览器本地自定义图片/视频模型,并把旧 localStorage 选中值回退到默认可用模型。
+
后端:api/main.py 的 video_model_options() 按真实模型去重:当前 kling、veo 等别名如果都指向 doubao-seedance-2-0-fast-260128,/health 只返回一个 Seedance 选项,不再假装有多个不同视频模型。
+
影响:员工在画布里只能选到当前可直接生成的图片/视频模型;后续新增 Kling、Veo 或其他图片模型时,必须先在后端完成真实模型配置和探针验证,再更新前端白名单。
+
+
2026-05-25 · 恢复上游画布能力,API 保持 SKG 接入
diff --git a/web/canvas-app/src/components/ApiSettings.vue b/web/canvas-app/src/components/ApiSettings.vue
index 5851903..ec01258 100644
--- a/web/canvas-app/src/components/ApiSettings.vue
+++ b/web/canvas-app/src/components/ApiSettings.vue
@@ -102,17 +102,6 @@
图片模型
{{ allImageModels.length }} 个
-
-
-
- 添加
-
-
视频模型
{{ allVideoModels.length }} 个
-
-
-
- 添加
-
-
{
@@ -273,20 +249,6 @@ const handleAddChatModel = () => {
}
}
-const handleAddImageModel = () => {
- if (newImageModel.value.trim()) {
- modelStore.addCustomImageModel(newImageModel.value.trim())
- newImageModel.value = ''
- }
-}
-
-const handleAddVideoModel = () => {
- if (newVideoModel.value.trim()) {
- modelStore.addCustomVideoModel(newVideoModel.value.trim())
- newVideoModel.value = ''
- }
-}
-
// Handle remove models | 处理删除模型
const handleRemoveChatModel = (modelKey) => {
modelStore.removeCustomChatModel(modelKey)
diff --git a/web/canvas-app/src/components/nodes/ImageConfigNode.vue b/web/canvas-app/src/components/nodes/ImageConfigNode.vue
index 19653ac..654df97 100644
--- a/web/canvas-app/src/components/nodes/ImageConfigNode.vue
+++ b/web/canvas-app/src/components/nodes/ImageConfigNode.vue
@@ -166,7 +166,7 @@ import { useImageGeneration } from '../../hooks'
import { updateNode, addNode, addEdge, nodes, edges, duplicateNode, removeNode } from '../../stores/canvas'
import NodeHandleMenu from './NodeHandleMenu.vue'
import { useModelStore } from '../../stores/pinia'
-import { getModelSizeOptions, getModelQualityOptions, getModelConfig, DEFAULT_IMAGE_MODEL } from '../../stores/models'
+import { getModelSizeOptions, getModelQualityOptions, getModelConfig, DEFAULT_IMAGE_MODEL, DEFAULT_IMAGE_SIZE } from '../../stores/models'
import { parseMentions } from '../../hooks/useNodeRef'
// 使用 Pinia store 获取模型选项(根据渠道过滤)
@@ -189,7 +189,7 @@ const { loading, error, images: generatedImages, generate } = useImageGeneration
// Local state | 本地状态
const showHandleMenu = ref(false)
const localModel = ref(props.data?.model || DEFAULT_IMAGE_MODEL)
-const localSize = ref(props.data?.size || '2048x2048')
+const localSize = ref(props.data?.size || DEFAULT_IMAGE_SIZE)
const localQuality = ref(props.data?.quality || 'standard')
// Label editing state | Label 编辑状态
@@ -288,7 +288,8 @@ onMounted(() => {
if (!localModel.value || !isModelAvailable) {
// 使用 store 中的默认模型或第一个可用模型
- localModel.value = modelStore.selectedImageModel || availableModels[0]?.key || DEFAULT_IMAGE_MODEL
+ const selected = availableModels.find(m => m.key === modelStore.selectedImageModel)?.key
+ localModel.value = selected || availableModels[0]?.key || DEFAULT_IMAGE_MODEL
updateNode(props.id, { model: localModel.value })
}
})
@@ -486,8 +487,7 @@ const handleModelSelect = (key) => {
let defaultSize = config?.defaultParams?.size
if (!defaultSize && newSizeOptions.length > 0) {
- // 备用逻辑:查找 2048 或最接近的尺寸
- defaultSize = newSizeOptions.find(o => o.key === '2048x2048')?.key
+ defaultSize = newSizeOptions.find(o => o.key === DEFAULT_IMAGE_SIZE)?.key
|| newSizeOptions.find(o => o.key.includes('1024'))?.key
|| newSizeOptions[0].key
}
@@ -508,7 +508,7 @@ const handleQualitySelect = (quality) => {
// Update size to first option of new quality | 更新尺寸为新画质的第一个选项
const newSizeOptions = getModelSizeOptions(localModel.value, quality)
if (newSizeOptions.length > 0) {
- const defaultSize = quality === '4k' ? newSizeOptions.find(o => o.key.includes('4096'))?.key || newSizeOptions[4]?.key : newSizeOptions[4]?.key
+ const defaultSize = newSizeOptions.find(o => o.key === DEFAULT_IMAGE_SIZE)?.key
localSize.value = defaultSize || newSizeOptions[0].key
updateNode(props.id, { quality, size: localSize.value })
} else {
diff --git a/web/canvas-app/src/components/nodes/ImageNode.vue b/web/canvas-app/src/components/nodes/ImageNode.vue
index caa48dc..c1ca598 100644
--- a/web/canvas-app/src/components/nodes/ImageNode.vue
+++ b/web/canvas-app/src/components/nodes/ImageNode.vue
@@ -417,8 +417,8 @@ const handleSelect = (item) => {
// Create imageConfig node
const configNodeId = addNode('imageConfig', { x: nodeX + 900, y: nodeY }, {
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048',
+ model: 'auto',
+ size: '1024x1536',
label: '生图配置'
})
@@ -626,8 +626,8 @@ const createInpaintWorkflow = () => {
// Create imageConfig node for inpainting | 创建图生图配置节点
const configNodeId = addNode('imageConfig', { x: nodeX + 600, y: nodeY }, {
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048',
+ model: 'auto',
+ size: '1024x1536',
label: '局部重绘',
inpaintMode: true
})
@@ -848,8 +848,8 @@ const handleImageGen = () => {
// Create imageConfig node for generation | 创建生图配置节点
const configNodeId = addNode('imageConfig', { x: nodeX + 900, y: nodeY }, {
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048',
+ model: 'auto',
+ size: '1024x1536',
label: '生图配置'
})
diff --git a/web/canvas-app/src/components/nodes/LLMConfigNode.vue b/web/canvas-app/src/components/nodes/LLMConfigNode.vue
index 05a955b..d31aa23 100644
--- a/web/canvas-app/src/components/nodes/LLMConfigNode.vue
+++ b/web/canvas-app/src/components/nodes/LLMConfigNode.vue
@@ -417,7 +417,7 @@ const handleSelect = (item) => {
const nodeY = currentNode?.position?.y || 0
const defaultData = {
- imageConfig: { model: 'doubao-seedream-4-5-251128', size: '2048x2048', label: '文生图' },
+ imageConfig: { model: 'auto', size: '1024x1536', label: '文生图' },
videoConfig: { label: '视频生成' },
text: { content: '', label: '文本输入' }
}
@@ -1003,8 +1003,8 @@ const handleSplitToTextWithImage = () => {
position: { x: baseX + colSpacing, y: segY },
data: {
label: `图片 ${i + 1}`,
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
}
nodeSpecs.push(imageConfigSpec)
diff --git a/web/canvas-app/src/components/nodes/TextNode.vue b/web/canvas-app/src/components/nodes/TextNode.vue
index a928ede..da4d97c 100644
--- a/web/canvas-app/src/components/nodes/TextNode.vue
+++ b/web/canvas-app/src/components/nodes/TextNode.vue
@@ -440,7 +440,7 @@ const handleSelect = (item) => {
const nodeY = currentNode?.position?.y || 0
const defaultData = {
- imageConfig: { model: 'doubao-seedream-4-5-251128', size: '2048x2048', label: '文生图' },
+ imageConfig: { model: 'auto', size: '1024x1536', label: '文生图' },
videoConfig: { label: '视频生成' },
llmConfig: { label: 'LLM文本生成' }
}
@@ -695,8 +695,8 @@ const handleImageGen = () => {
// Create imageConfig node | 创建text生图配置节点
const configNodeId = addNode('imageConfig', { x: nodeX + 400, y: nodeY }, {
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048',
+ model: 'auto',
+ size: '1024x1536',
label: '文生图'
})
diff --git a/web/canvas-app/src/components/nodes/VideoConfigNode.vue b/web/canvas-app/src/components/nodes/VideoConfigNode.vue
index 499b985..b8aa0e5 100644
--- a/web/canvas-app/src/components/nodes/VideoConfigNode.vue
+++ b/web/canvas-app/src/components/nodes/VideoConfigNode.vue
@@ -489,7 +489,8 @@ onMounted(() => {
if (!localModel.value || !isModelAvailable) {
// 使用 store 中的默认模型或第一个可用模型
- localModel.value = modelStore.selectedVideoModel || availableModels[0]?.key || DEFAULT_VIDEO_MODEL
+ const selected = availableModels.find(m => m.key === modelStore.selectedVideoModel)?.key
+ localModel.value = selected || availableModels[0]?.key || DEFAULT_VIDEO_MODEL
updateNode(props.id, { model: localModel.value })
}
})
diff --git a/web/canvas-app/src/config/models.js b/web/canvas-app/src/config/models.js
index 2dbaf72..6a85923 100644
--- a/web/canvas-app/src/config/models.js
+++ b/web/canvas-app/src/config/models.js
@@ -3,36 +3,20 @@
* Centralized model configuration | 集中模型配置
*/
-// Seedream image size options | 豆包图片尺寸选项
+// SKG backend image size options | SKG 后端图片尺寸选项
export const SEEDREAM_SIZE_OPTIONS = [
- { label: '21:9', key: '3024x1296' },
- { label: '16:9', key: '2560x1440' },
- { label: '4:3', key: '2304x1728' },
- { label: '3:2', key: '2496x1664' },
- { label: '1:1', key: '2048x2048' },
- { label: '2:3', key: '1664x2496' },
- { label: '3:4', key: '1728x2304' },
- { label: '9:16', key: '1440x2560' },
- { label: '9:21', key: '1296x3024' }
+ { label: '自动', key: 'auto' },
+ { label: '竖图 2:3', key: '1024x1536' },
+ { label: '方图 1:1', key: '1024x1024' },
+ { label: '横图 3:2', key: '1536x1024' }
]
-// Seedream 4K image size options | 豆包4K图片尺寸选项
-export const SEEDREAM_4K_SIZE_OPTIONS = [
- { label: '21:9', key: '6198x2656' },
- { label: '16:9', key: '5404x3040' },
- { label: '4:3', key: '4694x3520' },
- { label: '3:2', key: '4992x3328' },
- { label: '1:1', key: '4096x4096' },
- { label: '2:3', key: '3328x4992' },
- { label: '3:4', key: '3520x4694' },
- { label: '9:16', key: '3040x5404' },
- { label: '9:21', key: '2656x6198' }
-]
+// Kept for compatibility with upstream model helpers.
+export const SEEDREAM_4K_SIZE_OPTIONS = SEEDREAM_SIZE_OPTIONS
-// Seedream quality options | 豆包画质选项
+// SKG backend currently exposes model choice and size; quality is retained as a no-op UI field.
export const SEEDREAM_QUALITY_OPTIONS = [
- { label: '标准画质', key: 'standard' },
- { label: '4K 高清', key: '4k' }
+ { label: '标准', key: 'standard' }
]
export const BANANA_SIZE_OPTIONS = [
@@ -48,51 +32,37 @@ export const BANANA_SIZE_OPTIONS = [
// Image generation models | 图片生成模型
export const IMAGE_MODELS = [
{
- label: 'Nano Banana 2',
- key: 'nano-banana-2',
- provider: ['chatfire'], // 火宝渠道
- sizes: BANANA_SIZE_OPTIONS.map(s => s.key),
- // qualities: SEEDREAM_QUALITY_OPTIONS,
- // getSizesByQuality: (quality) => quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS,
- defaultParams: {
- size: '1x1',
- quality: 'standard',
- style: 'vivid'
- }
- },
- {
- label: 'Nano Banana Pro',
- key: 'nano-banana-pro',
- provider: ['chatfire'], // 火宝渠道
- sizes: BANANA_SIZE_OPTIONS.map(s => s.key),
- // qualities: SEEDREAM_QUALITY_OPTIONS,
- // getSizesByQuality: (quality) => quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS,
- defaultParams: {
- size: '1x1',
- quality: 'standard',
- style: 'vivid'
- }
- },
- {
- label: '豆包 Seedream 4.5',
- key: 'doubao-seedream-4-5-251128',
- provider: ['chatfire'], // 火宝渠道
+ label: '自动',
+ key: 'auto',
+ provider: ['chatfire'],
sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
qualities: SEEDREAM_QUALITY_OPTIONS,
- getSizesByQuality: (quality) => quality === '4k' ? SEEDREAM_4K_SIZE_OPTIONS : SEEDREAM_SIZE_OPTIONS,
defaultParams: {
- size: '2048x2048',
+ size: '1024x1536',
quality: 'standard',
style: 'vivid'
}
},
{
- label: 'Nano Banana',
- key: 'nano-banana',
- provider: ['chatfire'], // 火宝渠道
- tips: '尺寸写在提示词中: 尺寸 9:16',
- sizes: [],
+ label: 'GPT Image 2',
+ key: 'gpt-image-2',
+ provider: ['chatfire'],
+ sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
+ qualities: SEEDREAM_QUALITY_OPTIONS,
defaultParams: {
+ size: '1024x1536',
+ quality: 'standard',
+ style: 'vivid'
+ }
+ },
+ {
+ label: 'Gemini 图片',
+ key: 'gemini-3-pro-image-preview',
+ provider: ['chatfire'],
+ sizes: SEEDREAM_SIZE_OPTIONS.map(s => s.key),
+ qualities: SEEDREAM_QUALITY_OPTIONS,
+ defaultParams: {
+ size: '1024x1536',
quality: 'standard',
style: 'vivid'
}
@@ -102,11 +72,10 @@ export const IMAGE_MODELS = [
// Video ratio options | 视频比例选项
export const VIDEO_RATIO_LIST = [
- { label: '16:9 (横版)', key: '16x9' },
- { label: '4:3', key: '4x3' },
- { label: '1:1 (方形)', key: '1x1' },
- { label: '3:4', key: '3x4' },
- { label: '9:16 (竖版)', key: '9x16' }
+ { label: '竖屏 9:16', key: '720x1280' },
+ { label: '横屏 16:9', key: '1280x720' },
+ { label: '方形 1:1', key: '1024x1024' },
+ { label: '竖屏 3:4', key: '960x1280' }
]
// Video resolution options for Seedance | Seedance 分辨率选项
@@ -118,102 +87,23 @@ export const SEEDANCE_RESOLUTION_OPTIONS = [
// Video generation models | 视频生成模型
export const VIDEO_MODELS = [
- // Seedance 模型 - 1.5 Pro
{
- label: 'Seedance 1.5 Pro (图文视频)',
- key: 'doubao-seedance-1-5-pro-251215',
+ label: 'Seedance 2.0 Fast',
+ key: 'seedance',
provider: ['chatfire'],
type: 't2v+i2v',
- ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- resolutions: ['480p', '720p', '1080p'],
- defaultResolution: '1080p',
- defaultParams: { ratio: '16:9', duration: 10, resolution: '1080p' }
- },
- // Seedance 模型 - 文生视频
- {
- label: 'Seedance 1.0 Lite (文生视频)',
- key: 'doubao-seedance-1-0-lite-t2v-250428',
- provider: ['chatfire'],
- type: 't2v', // 文生视频
- ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- resolutions: ['480p', '720p', '1080p'],
+ ratios: ['720x1280', '1280x720', '1024x1024', '960x1280'],
+ durs: [
+ { label: '5 秒', key: 5 },
+ { label: '8 秒', key: 8 },
+ { label: '10 秒', key: 10 },
+ { label: '12 秒', key: 12 },
+ { label: '15 秒', key: 15 }
+ ],
+ resolutions: ['720p'],
defaultResolution: '720p',
- defaultParams: { ratio: '16:9', duration: 5, resolution: '720p' }
+ defaultParams: { ratio: '720x1280', duration: 10, resolution: '720p' }
},
- // Seedance 模型 - 图生视频
- {
- label: 'Seedance 1.0 Lite (图生视频)',
- key: 'doubao-seedance-1-0-lite-i2v-250428',
- provider: ['chatfire'],
- type: 'i2v', // 图生视频
- ratios: ['16:9'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- resolutions: ['480p', '720p', '1080p'],
- defaultResolution: '720p',
- defaultParams: { ratio: '16:9', duration: 5, resolution: '720p' }
- },
- // Seedance 模型 - 图文视频 Pro
- {
- label: 'Seedance 1.0 Pro (图文视频)',
- key: 'doubao-seedance-1-0-pro-250528',
- provider: ['chatfire'],
- type: 't2v+i2v', // 图文视频
- ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9', '16:9'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- resolutions: ['480p', '720p', '1080p'],
- defaultResolution: '1080p',
- defaultParams: { ratio: '16:9', duration: 5, resolution: '1080p' }
- },
-
- // Seedance 模型 - 1.0 Pro Fast
- {
- label: 'Seedance 1.0 Pro Fast (图文视频)',
- key: 'doubao-seedance-1-0-pro-fast-251015',
- provider: ['chatfire'],
- type: 't2v+i2v',
- ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- resolutions: ['480p', '720p', '1080p'],
- defaultResolution: '1080p',
- defaultParams: { ratio: '16:9', duration: 5, resolution: '1080p' }
- },
- // 可灵 Kling
- // {
- // label: '可灵 Kling v2.5-turbo',
- // key: 'kling-v2-1',
- // provider: ['chatfire'], // 仅火宝渠道
- // ratios: VIDEO_RATIO_LIST.map(s => s.key),
- // durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- // defaultParams: { ratio: '9:16', duration: 10 }
- // },
- // {
- // label: 'runway/gen4-turbo',
- // key: 'runway/gen4-turbo',
- // ratios: VIDEO_RATIO_LIST.map(s => s.key),
- // durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- // defaultParams: { ratio: '16:9', duration: 5 }
- // },
- // {
- // label: '可灵视频 O1',
- // key: 'kling-video-o1',
- // ratios: VIDEO_RATIO_LIST.map(s => s.key),
- // durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- // defaultParams: { ratio: '16:9', duration: 5 }
- // },
- // {
- // label: 'viduq2-pro_720p', key: 'viduq2-pro_720p',
- // ratios: VIDEO_RATIO_LIST.map(s => s.key),
- // durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- // defaultParams: { ratio: '16:9', duration: 5 }
- // },
- // {
- // label: 'Sora 2', key: 'sora-2',
- // ratios: VIDEO_RATIO_LIST.map(s => s.key),
- // durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- // defaultParams: { ratio: '16:9', duration: 5 }
- // }
]
// Chat/LLM models | 对话模型
@@ -228,9 +118,10 @@ export const CHAT_MODELS = [
// Image size options | 图片尺寸选项
export const IMAGE_SIZE_OPTIONS = [
- { label: '2048x2048', key: '2048x2048' },
- { label: '1792x1024 (横版)', key: '1792x1024' },
- { label: '1024x1792 (竖版)', key: '1024x1792' }
+ { label: '自动', key: 'auto' },
+ { label: '竖图 2:3', key: '1024x1536' },
+ { label: '方图 1:1', key: '1024x1024' },
+ { label: '横图 3:2', key: '1536x1024' }
]
// Image quality options | 图片质量选项
@@ -251,16 +142,19 @@ export const VIDEO_RATIO_OPTIONS = VIDEO_RATIO_LIST
// Video duration options | 视频时长选项
export const VIDEO_DURATION_OPTIONS = [
{ label: '5 秒', key: 5 },
- { label: '10 秒', key: 10 }
+ { label: '8 秒', key: 8 },
+ { label: '10 秒', key: 10 },
+ { label: '12 秒', key: 12 },
+ { label: '15 秒', key: 15 }
]
// Default values | 默认值
-export const DEFAULT_IMAGE_MODEL = 'nano-banana-pro'
-export const DEFAULT_VIDEO_MODEL = 'doubao-seedance-1-5-pro-251215'
+export const DEFAULT_IMAGE_MODEL = 'auto'
+export const DEFAULT_VIDEO_MODEL = 'seedance'
export const DEFAULT_CHAT_MODEL = 'gpt-4o-mini'
-export const DEFAULT_IMAGE_SIZE = '2048x2048'
-export const DEFAULT_VIDEO_RATIO = '16:9'
-export const DEFAULT_VIDEO_DURATION = 5
+export const DEFAULT_IMAGE_SIZE = '1024x1536'
+export const DEFAULT_VIDEO_RATIO = '720x1280'
+export const DEFAULT_VIDEO_DURATION = 10
// Get model by key | 根据 key 获取模型
export const getModelByName = (key) => {
diff --git a/web/canvas-app/src/config/workflows.js b/web/canvas-app/src/config/workflows.js
index d3a92bd..83d0868 100644
--- a/web/canvas-app/src/config/workflows.js
+++ b/web/canvas-app/src/config/workflows.js
@@ -81,8 +81,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + nodeSpacing, y: startPosition.y + rowSpacing * 1.5 },
data: {
label: '主角色图',
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -145,8 +145,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: currentX, y: angleY },
data: {
label: `${angleConfig.label} (${angleConfig.english})`,
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -281,8 +281,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y },
data: {
label: '生成模特图',
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -294,8 +294,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y + rowSpacing },
data: {
label: '侧面展示图',
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -307,8 +307,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y + rowSpacing * 2 },
data: {
label: '俯瞰展示图',
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -320,8 +320,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y + rowSpacing * 3 },
data: {
label: '拆解图',
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -507,8 +507,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y },
data: {
label: '生成正面全身图',
- model: 'doubao-seedream-4-5-251128',
- size: '1440x2560'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -569,8 +569,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing },
data: {
label: '侧面半身图',
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -582,8 +582,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing * 2 },
data: {
label: '表情特写图',
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -595,8 +595,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing * 3 },
data: {
label: '背面全身图',
- model: 'doubao-seedream-4-5-251128',
- size: '1440x2560'
+ model: 'auto',
+ size: '1024x1536'
}
})
@@ -753,8 +753,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y },
data: {
label: '生成基础场景',
- model: 'doubao-seedream-4-5-251128',
- size: '2560x1440'
+ model: 'auto',
+ size: '1536x1024'
}
})
@@ -815,8 +815,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing },
data: {
label: '傍晚场景',
- model: 'doubao-seedream-4-5-251128',
- size: '2560x1440'
+ model: 'auto',
+ size: '1536x1024'
}
})
@@ -828,8 +828,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing * 2 },
data: {
label: '夜晚场景',
- model: 'doubao-seedream-4-5-251128',
- size: '2560x1440'
+ model: 'auto',
+ size: '1536x1024'
}
})
@@ -841,8 +841,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 4 + 100, y: startPosition.y + rowSpacing * 3 },
data: {
label: '雨天场景',
- model: 'doubao-seedream-4-5-251128',
- size: '2560x1440'
+ model: 'auto',
+ size: '1536x1024'
}
})
@@ -996,8 +996,8 @@ export const WORKFLOW_TEMPLATES = [
// position: { x: startPosition.x + colSpacing * 2, y: startPosition.y + rowSpacing },
// data: {
// label: '分镜画面',
- // model: 'doubao-seedream-4-5-251128',
- // size: '2560x1440'
+ // model: 'auto',
+ // size: '1536x1024'
// }
// })
@@ -1123,8 +1123,8 @@ export const WORKFLOW_TEMPLATES = [
position: { x: startPosition.x + colSpacing * 2, y: startPosition.y - rowSpacing },
data: {
label: '角色参考图',
- model: 'doubao-seedream-4-5-251128',
- size: '2048x2048'
+ model: 'auto',
+ size: '1024x1536'
}
})
diff --git a/web/canvas-app/src/hooks/useModelConfig.js b/web/canvas-app/src/hooks/useModelConfig.js
index 989b40f..ad7f6d5 100644
--- a/web/canvas-app/src/hooks/useModelConfig.js
+++ b/web/canvas-app/src/hooks/useModelConfig.js
@@ -79,6 +79,11 @@ const setStored = (key, value) => {
}
}
+const getValidStoredModel = (key, defaultValue, builtInModels) => {
+ const stored = getStored(key, defaultValue)
+ return builtInModels.some(model => model.key === stored) ? stored : defaultValue
+}
+
// Shared reactive state (singleton pattern)
const customChatModels = ref(getStoredJson(STORAGE_KEYS.CUSTOM_CHAT_MODELS, []))
const customImageModels = ref(getStoredJson(STORAGE_KEYS.CUSTOM_IMAGE_MODELS, []))
@@ -90,8 +95,8 @@ const customImageModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_IMAGE_
const customVideoModelsByProvider = ref(getStoredJson(STORAGE_KEYS.CUSTOM_VIDEO_MODELS_BY_PROVIDER || 'custom-video-models-by-provider', {}))
const selectedChatModel = ref(getStored(STORAGE_KEYS.SELECTED_CHAT_MODEL, DEFAULT_CHAT_MODEL))
-const selectedImageModel = ref(getStored(STORAGE_KEYS.SELECTED_IMAGE_MODEL, DEFAULT_IMAGE_MODEL))
-const selectedVideoModel = ref(getStored(STORAGE_KEYS.SELECTED_VIDEO_MODEL, DEFAULT_VIDEO_MODEL))
+const selectedImageModel = ref(getValidStoredModel(STORAGE_KEYS.SELECTED_IMAGE_MODEL, DEFAULT_IMAGE_MODEL, IMAGE_MODELS))
+const selectedVideoModel = ref(getValidStoredModel(STORAGE_KEYS.SELECTED_VIDEO_MODEL, DEFAULT_VIDEO_MODEL, VIDEO_MODELS))
/**
* Model Configuration Hook
@@ -117,47 +122,13 @@ export const useModelConfig = () => {
}))
])
- const allImageModels = computed(() => [
- ...IMAGE_MODELS.map(m => ({ ...m, isCustom: false })),
- ...customImageModels.value.map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- sizes: [],
- defaultParams: { quality: 'standard', style: 'vivid' }
- })),
- // 添加当前渠道的自定义模型
- ...(customImageModelsByProvider.value[currentProvider.value] || []).map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- sizes: [],
- defaultParams: { quality: 'standard', style: 'vivid' },
- provider: [currentProvider.value]
- }))
- ])
+ const allImageModels = computed(() =>
+ IMAGE_MODELS.map(m => ({ ...m, isCustom: false }))
+ )
- const allVideoModels = computed(() => [
- ...VIDEO_MODELS.map(m => ({ ...m, isCustom: false })),
- ...customVideoModels.value.map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- ratios: ['16x9', '9:16', '1:1'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- defaultParams: { ratio: '16:9', duration: 5 }
- })),
- // 添加当前渠道的自定义模型
- ...(customVideoModelsByProvider.value[currentProvider.value] || []).map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- ratios: ['16x9', '9:16', '1:1'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- defaultParams: { ratio: '16:9', duration: 5 },
- provider: [currentProvider.value]
- }))
- ])
+ const allVideoModels = computed(() =>
+ VIDEO_MODELS.map(m => ({ ...m, isCustom: false }))
+ )
// Available models filtered by provider | 根据渠道过滤的可用模型
const availableChatModels = computed(() =>
@@ -188,29 +159,12 @@ export const useModelConfig = () => {
provider: [provider]
}))
]
- const image = [
- ...IMAGE_MODELS.filter(m => isModelSupported(m, provider)).map(m => ({ ...m, isCustom: false })),
- ...(customImageModelsByProvider.value[provider] || []).map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- sizes: [],
- defaultParams: { quality: 'standard', style: 'vivid' },
- provider: [provider]
- }))
- ]
- const video = [
- ...VIDEO_MODELS.filter(m => isModelSupported(m, provider)).map(m => ({ ...m, isCustom: false })),
- ...(customVideoModelsByProvider.value[provider] || []).map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- ratios: ['16x9', '9:16', '1:1'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- defaultParams: { ratio: '16:9', duration: 5 },
- provider: [provider]
- }))
- ]
+ const image = IMAGE_MODELS
+ .filter(m => isModelSupported(m, provider))
+ .map(m => ({ ...m, isCustom: false }))
+ const video = VIDEO_MODELS
+ .filter(m => isModelSupported(m, provider))
+ .map(m => ({ ...m, isCustom: false }))
return { chat, image, video }
}
diff --git a/web/canvas-app/src/stores/canvas.js b/web/canvas-app/src/stores/canvas.js
index d37a2fe..d2c73f3 100644
--- a/web/canvas-app/src/stores/canvas.js
+++ b/web/canvas-app/src/stores/canvas.js
@@ -401,7 +401,7 @@ export const initSampleData = () => {
// Add image config node | 添加文生图配置节点
addNode('imageConfig', { x: 450, y: 150 }, {
prompt: '',
- model: 'doubao-seedream-4-5-251128',
+ model: 'auto',
ratio: '16:9 | 4张 | 高清',
label: '文生图'
})
diff --git a/web/canvas-app/src/stores/pinia/models.js b/web/canvas-app/src/stores/pinia/models.js
index cd9a09c..ef1a2bc 100644
--- a/web/canvas-app/src/stores/pinia/models.js
+++ b/web/canvas-app/src/stores/pinia/models.js
@@ -88,6 +88,11 @@ const setStoredJson = (key, value) => {
}
}
+const getValidStoredModel = (key, defaultValue, builtInModels) => {
+ const stored = getStored(key, defaultValue)
+ return builtInModels.some(model => model.key === stored) ? stored : defaultValue
+}
+
/**
* 检查模型是否支持指定渠道
*/
@@ -160,8 +165,8 @@ export const useModelStore = defineStore('model', () => {
// 选中的模型
const selectedChatModel = ref(getStored(STORAGE_KEYS.SELECTED_CHAT_MODEL, DEFAULT_CHAT_MODEL))
- const selectedImageModel = ref(getStored(STORAGE_KEYS.SELECTED_IMAGE_MODEL, DEFAULT_IMAGE_MODEL))
- const selectedVideoModel = ref(getStored(STORAGE_KEYS.SELECTED_VIDEO_MODEL, DEFAULT_VIDEO_MODEL))
+ const selectedImageModel = ref(getValidStoredModel(STORAGE_KEYS.SELECTED_IMAGE_MODEL, DEFAULT_IMAGE_MODEL, IMAGE_MODELS))
+ const selectedVideoModel = ref(getValidStoredModel(STORAGE_KEYS.SELECTED_VIDEO_MODEL, DEFAULT_VIDEO_MODEL, VIDEO_MODELS))
// 按渠道存储的 API 配置
const apiKeysByProvider = ref(getStoredJson(STORAGE_KEYS.API_KEYS_BY_PROVIDER, {}))
@@ -205,47 +210,13 @@ export const useModelStore = defineStore('model', () => {
}))
])
- const allImageModels = computed(() => [
- ...IMAGE_MODELS.map(m => ({ ...m, isCustom: false })),
- ...customImageModels.value.map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- sizes: [],
- defaultParams: { quality: 'standard', style: 'vivid' }
- })),
- // 添加当前渠道的自定义模型
- ...(customImageModelsByProvider.value[currentProvider.value] || []).map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- sizes: [],
- defaultParams: { quality: 'standard', style: 'vivid' },
- provider: [currentProvider.value]
- }))
- ])
+ const allImageModels = computed(() =>
+ IMAGE_MODELS.map(m => ({ ...m, isCustom: false }))
+ )
- const allVideoModels = computed(() => [
- ...VIDEO_MODELS.map(m => ({ ...m, isCustom: false })),
- ...customVideoModels.value.map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- ratios: ['16x9', '9:16', '1:1'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- defaultParams: { ratio: '16:9', duration: 5 }
- })),
- // 添加当前渠道的自定义模型
- ...(customVideoModelsByProvider.value[currentProvider.value] || []).map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- ratios: ['16x9', '9:16', '1:1'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- defaultParams: { ratio: '16:9', duration: 5 },
- provider: [currentProvider.value]
- }))
- ])
+ const allVideoModels = computed(() =>
+ VIDEO_MODELS.map(m => ({ ...m, isCustom: false }))
+ )
// ============ Computed: Available Models (filtered by provider) ============
@@ -412,29 +383,12 @@ export const useModelStore = defineStore('model', () => {
provider: [provider]
}))
]
- const image = [
- ...IMAGE_MODELS.filter(m => isModelSupported(m, provider)).map(m => ({ ...m, isCustom: false })),
- ...(customImageModelsByProvider.value[provider] || []).map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- sizes: [],
- defaultParams: { quality: 'standard', style: 'vivid' },
- provider: [provider]
- }))
- ]
- const video = [
- ...VIDEO_MODELS.filter(m => isModelSupported(m, provider)).map(m => ({ ...m, isCustom: false })),
- ...(customVideoModelsByProvider.value[provider] || []).map(m => ({
- label: m.label || m.key,
- key: m.key,
- isCustom: true,
- ratios: ['16x9', '9:16', '1:1'],
- durs: [{ label: '5 秒', key: 5 }, { label: '10 秒', key: 10 }],
- defaultParams: { ratio: '16:9', duration: 5 },
- provider: [provider]
- }))
- ]
+ const image = IMAGE_MODELS
+ .filter(m => isModelSupported(m, provider))
+ .map(m => ({ ...m, isCustom: false }))
+ const video = VIDEO_MODELS
+ .filter(m => isModelSupported(m, provider))
+ .map(m => ({ ...m, isCustom: false }))
return { chat, image, video }
}
diff --git a/web/canvas-app/src/stores/projects.js b/web/canvas-app/src/stores/projects.js
index b285370..5072d13 100644
--- a/web/canvas-app/src/stores/projects.js
+++ b/web/canvas-app/src/stores/projects.js
@@ -345,8 +345,8 @@ export const initProjectsStore = () => {
position: { x: 500, y: 150 },
data: {
prompt: '',
- model: 'doubao-seedream-4-5-251128',
- size: '512x512',
+ model: 'auto',
+ size: '1024x1024',
label: '文生图'
}
}