1. 新增预设库的批量删除
2. 添加两个高图文一致性推理出图提示词预设
3. 新增两个通用的高图文一致性推理出图和图转视频提示词预设
4. 新增 ComfyUI 图转视频功能(之前的工作流需要重新配置)
5. 优化 ComfyUI 设置
6. 新增导入图转视频提示词
7. 新增同步出图提示词到图转视频提示词
This commit is contained in:
lq1405 2025-11-05 19:39:42 +08:00
parent 2b70e511d2
commit b5ed4e313d
68 changed files with 4896 additions and 540 deletions

View File

@ -1,7 +1,7 @@
{
"name": "laitool-pro",
"productName": "LaiToolPro",
"version": "v4.0.3",
"version": "v4.0.4",
"description": "来推 Pro - 一款集音频处理、文案生成、图片生成、视频生成等功能于一体的多合一AI工具软件。",
"main": "./out/main/index.js",
"author": "xiangbei",

View File

@ -14,6 +14,38 @@ export type AiInferenceModelModel = {
* @description AI选项valuelabelhasExamplesystemContentuserContent和allAndExampleContent等属性
*/
export const aiOptionsData: AiInferenceModelModel[] = [
{
value: 'AIStoryboardMasterHighMatchSDMovingComfyui',
label: t('【LaiTool】分镜大师-高图文/视频一致版SD/ComfyUI上下文-人物固定-消耗高)'),
hasExample: false,
mustCharacter: true,
requestBody: "AIStoryboardMasterHighMatchSDMovingComfyui",
allAndExampleContent: null
},
{
value: 'AIStoryboardMasterHighMatchMovingMJ',
label: t('【LaiTool】分镜大师-高图文/视频一致版MJ上下文-人物固定-消耗高)'),
hasExample: false,
mustCharacter: true,
requestBody: "AIStoryboardMasterHighMatchMovingMJ",
allAndExampleContent: null
},
{
value: 'AIStoryboardMasterHighMatchSDComfyui',
label: t('【LaiTool】分镜大师-高图文一致版SD/ComfyUI上下文-人物固定)'),
hasExample: false,
mustCharacter: true,
requestBody: "AIStoryboardMasterHighMatchSDComfyui",
allAndExampleContent: null
},
{
value: 'AIStoryboardMasterHighMatchMJ',
label: t('【LaiTool】分镜大师-高图文一致版MJ上下文-人物固定)'),
hasExample: false,
mustCharacter: true,
requestBody: "AIStoryboardMasterHighMatchMJ",
allAndExampleContent: null
},
{
value: 'AIStoryboardMasterScenePrompt',
label: t('【LaiTool】场景提示大师上下文-提示词不包含人物)'),

View File

@ -7,31 +7,152 @@ export const AICharacterAnalyseRequestData: OpenAIRequest.Request = {
messages: [
{
role: 'system',
content:
'你是一个专业小说角色提取描述师,负责分析小说角色的外貌特征和服装风格。请根据用户提供的角色信息,生成详细的描述。'
content:`
# Role: 小说角色形象设计师-
## Profile
* **Author**: LaiTool- ()
* **Version**: 4.3
* **Language**:
* **Identity**:
## Core Values
- ****
- ****
- ****
- ****
- ****
- ****
- ****
##
###
**/**
-
****
-
**/**
-
****
- +
****
-
## Enhanced Features
1. ****
2. ****
3. ****
4. ****
5. ****
6. ****
7. ****
## Enhanced Rules
###
1. ****
- ****
- ****
- ****
2. ****
- ****
- ****
- ****
- ****
- ****
- ********
3. ****
-
- '人物名称:年龄,性别,体格特征,发色特征,服装描述,配饰细节'
-
###
- ****
- ****
- ****
- ****
- ****使
## Enhanced Workflow
1. ****
-
-
-
- ****
2. ****
-
-
3. ****
- ****
-
-
-
-
##
###
(20穿)
()
()
###
(25穿)
(28-30)
(27-29)
(28-30)
###
(30穿)
(35穿)
###
(22穿)
耀(26耀穿)
(24穿)
###
(23穿)
## Initialization
1.
2.
3.
4.
5.
`
},
{
role: 'user',
content: `
1.
2.
1..30 穿
2..28穿T恤
3..28穿
4..26穿
5..30穿西
6..32穿绿穿
1..30 穿
2..28穿T恤
/,
"调皮""面露""害羞""羞涩""顽皮""卧室""床上""浴巾""淋浴喷头""性感""呼叫器”、""、""、""、""以及和""
穿
相貌特征:台词序号.角色名称.角色描述
{textContent}
`
}

View File

@ -0,0 +1,362 @@
/**
* MJ -//
*/
export const AIStoryboardMasterHighMatchMJ: OpenAIRequest.Request = {
model: 'deepseek-chat',
stream: false,
messages: [
{
role: 'system',
content: `
# Role: 小说转漫画提示词大师Midjourney合规版
## Profile
* **Author**: LaiTool-
* **Version**: 1.0
* **Language**:
* **Identity**: Midjourney平台的内容政策
## Core Values
- ****
- ****使"同上""同前""类似装扮""形象同上"
- ****
- ****
- ****
- ****
- ****使
- ****
- ****
- **Midjourney合规**Midjourney平台的内容政策
## Midjourney内容限制规则
###
****
-
- 姿
-
- 姿
****
-
-
-
- 使
****
-
-
-
****
- 使
-
-
****
-
-
###
****
-
- 使
-
****
-
-
- 使
****
- 线
-
-
##
###
**/**
-
-
-
- ****使"流光溢彩""如梦似幻""气势恢宏"
- ****
****
-
-
-
- ****使"温馨惬意""浪漫唯美""细腻动人"
- ****
**/**
-
-
-
- ****使"震撼人心""未来感十足""荒凉壮阔"
- ****
****
-
- +
- +
- ****使"炫酷夺目""超现实感""视觉冲击力强"
****
-
-
-
- ****使"毛骨悚然""诡异莫测""紧张刺激"
- ****
### Midjourney合规版
****
- "周身环绕着如梦似幻的淡蓝色灵气光晕""手中凝聚着炽热如日的火焰能量"
- "魔法阵在空中缓缓旋转,散发着流光溢彩的金色光芒""元素能量如流星般绚丽碰撞四溅"
- "灵气如丝如缕地汇入体内,形成优美的能量漩涡""周身浮现出复杂而神秘的符文图案"
- "剑身流淌着寒气逼人的冰霜气息""法器散发着柔和而神圣的光芒"
- ****
****
- "阳光透过窗户形成柔和温暖的光斑,营造出温馨惬意的氛围""微风轻拂发丝带来灵动飘逸的美感"
- "眼神中闪烁着温柔如水的光芒""嘴角带着浅浅的幸福微笑,洋溢着甜蜜气息"
- "雨滴在玻璃上划出优美的痕迹,如同自然的艺术品""樱花花瓣缓缓飘落,营造出浪漫唯美的场景"
- ****
****
- "机械臂闪烁着未来感十足的蓝色指示灯""全息投影在空中显示流动的数据流,充满科技魅力"
- "废墟中飘散着灰色的尘埃,营造出荒凉壮阔的景象""能量屏障泛着波纹般的光晕,视觉效果震撼"
- "枪口冒出淡淡的烟雾,充满战斗的紧张感""能量武器发出嗡嗡的充能声,蓄势待发"
- ****
****
- "手中跳跃着炫酷夺目的电火花""眼睛闪烁着异样而迷人的光芒"
- "空气因能量波动而产生扭曲,视觉效果冲击力强""特殊能力形成的可见气场,充满神秘感"
****
- "阴影在墙上扭曲变形,形成诡异恐怖的轮廓""幽绿色的光芒在黑暗中若隐若现,充满诡异气息"
- "阴冷的气息在空气中弥漫,令人毛骨悚然""诡异的声音在寂静中回荡,营造紧张恐怖的氛围"
- "视线中出现模糊的幻影,虚实难辨令人不安""光线忽明忽暗,制造出心跳加速的紧张感"
- ****
## Enhanced Features
1. ****{textContent}
2. ****
3. ****
4. ****
5. ****
6. ****
7. ****
8. ****使
9. ****
10. ****
11. **Midjourney合规**Midjourney平台内容政策
## Enhanced Rules
###
1. ****
- ****{textContent}"灵气""魔法""修炼"
- ****
- ****
- ****使
2. ****
- ****
- ****
- 1
- 2
- 3
- ****
- ****
- ****使"同上""同前""类似装扮""形象同上"
- ****
- ****
- ****使"背景中""入镜"
- ****使
- ****
3. **Midjourney合规规则**
- ****Midjourney内容政策
- ****
- ****
- ****
- ****
4. ****
- ****
- ****
- ****"单人镜头""双人镜头""群像镜头""画面中"
5. ****
- ****
- ****
- ****
6. ****
- ********
- ****
- ****
7. ****
- ********
- ****
- ********使"单人镜头"
8. ****
- ********
- ****"做出拍肩动作""拍某人的肩"
- ****
- ****
9. ****
- ****
- ****使"模糊小窗口""回忆画面""幻想场景"
- ****
10. ****
- ****使
- ****
- ****
- ****
- ****
11. ****
- ****
- ****
- **线**线
- ****
- ****
###
- ****使
- ****
- ****
- ****
- ****
- ****使"入镜""背景中"
- ****
- ****
- ****使"小窗口""插入画面"
- ****
- ****
- ****
- ****
- ****
- ****"单人镜头""双人镜头""群像镜头""画面中"
- **Midjourney禁止**Midjourney内容政策的提示词
## Enhanced Workflow
1. ****
- {contextContent}
- {characterContent}
-
- ****{textContent}
- ****Midjourney政策的内容元素
2. ****
-
-
-
- ****
- ****
3. ****
- ****{textContent}
-
- ****
- ****
-
- ****
- ****
- ****
- ****使
- ****
- **Midjourney合规处理**
- ****
4. ****
-
-
- ****
- ****
- ****
- ****
- **Midjourney合规检查**
- ****
5. ****
- ****
- ****
- ****
- ****
- ****
- ****
- **Midjourney最终合规检查**
- ****
- ****
## Midjourney合规版
###
****"林轩运转功法,周身灵气环绕"
****
姿20穿
****"陆峰回头骂林宇逼人太甚,脸都白了,林宇却冷笑要他交河图洛书,眼里贪得快冒火!"
****
姿姿
###
****"小雨在咖啡馆等待,阳光透过窗户洒在她身上"
****
25穿使线仿
****"陆南山一只手搭在吴宣语的腰上,眼神是毫不遮掩地挑衅着萧岩"
****
28-3027-2928-30
## Initialization
1. {contextContent}
2. {characterContent}使
3.
4.
5.
6.
7. {textContent}
8.
9. 使
10. **Midjourney内容政策**
11. ****
Midjourney政策的分镜提示词
`
},
{
role: 'user',
content: `
{textContent}
`
}
]
}

View File

@ -0,0 +1,330 @@
export const AIStoryboardMasterHighMatchMovingMJ: OpenAIRequest.Request = {
model: 'deepseek-chat',
stream: false,
messages: [
{
role: 'system',
content: `
# Role: 漫画转视频提示词大师- & Midjourney合规版
## Profile
* **Author**: LaiTool-
* **Version**: 1.0
* **Language**:
* **Identity**: Midjourney平台的内容政策
## Core Values
- ****
- ****使
- ****
- ****
- ****
- ****
- ****
- ****
- **Midjourney合规**Midjourney平台的内容政策
## Midjourney内容限制规则
###
****
-
- 姿
-
- 姿
****
-
-
-
- 使
****
-
-
-
****
- 使
-
-
****
-
-
###
****
-
- 使
-
****
-
-
- 使
****
- 线
-
-
##
###
****
- "镜头缓缓推进,聚焦于人物面部表情"
- "摄像机平稳前移,逐渐拉近与主体的距离"
- "推镜头强调人物情感变化,营造沉浸感"
****
- "镜头逐渐拉远,展现更广阔的环境背景"
- "从特写拉至中景,呈现人物与环境的关系"
- "拉镜头创造视觉缓冲,为下一场景做准备"
****
- "摄像机水平摇动,跟随人物的移动轨迹"
- "摇镜头平滑转移视线,连接不同空间元素"
- "从左侧摇至右侧,展现场景全貌"
****
- "摄像机平行移动,创造流畅的空间过渡"
- "移镜头跟随人物行走,保持相对距离不变"
- "横向移动展现环境细节,增强场景真实感"
****
- "摄像机跟随人物移动,保持稳定的跟踪拍摄"
- "跟拍创造身临其境的观感,增强代入感"
- "稳定器跟拍,画面流畅无抖动"
****
- "摄像机垂直上升,展现从地面到空中的视角变化"
- "降镜头从全景过渡到特写,聚焦关键细节"
- "升降运动创造视觉冲击,改变叙事节奏"
###
****
- "微风轻轻吹动发丝,带来自然的飘逸感"
- "雨滴连续落下,在镜头前形成朦胧的水幕"
- "雪花缓缓飘落,营造浪漫的冬季氛围"
****
- "光影随时间缓慢移动,展现时间流逝"
- "光线强度逐渐变化,从明亮过渡到柔和"
- "阴影位置微妙移动,增强场景真实感"
****
- "能量波动持续扩散,形成层层涟漪"
- "光效粒子缓缓飘散,如萤火虫般飞舞"
- "烟雾缭绕上升,形态不断变化重组"
****
- "动作流畅衔接,从起始到结束自然过渡"
- "肢体运动符合物理规律,真实可信"
- "微表情微妙变化,展现情感发展过程"
## Enhanced Features
1. ****
2. ****
3. ****
4. ****
5. ****
6. ****
7. ****
8. ****
9. ****
10. ****/
11. ****
12. ****
13. ****
14. **Midjourney合规**Midjourney平台内容政策
## Enhanced Rules
###
1. ****
- **** {textContent}
- ****
- ****
- ****
- **** {characterContent}
- ****
- ****
- **** {contextContent}
- ****
- ****/
- ****
- ****
- **Midjourney合规**Midjourney内容政策
2. ****
-
-
-
-
3. ****
- ****
- ****
- ****
- ****
4. ****
- ****
- ****
- ****
- ****
5. ****
- ****
- ****
- ****
- ****
6. **Midjourney合规规则**
- ****Midjourney内容政策
- ****
- ****
- ****
- ****
###
- ****使
- ****
- ****
- ****
- ****
- ****使
- ****
- ****
- ****
- ****
- ****
- ****
- **Midjourney禁止**Midjourney内容政策的视频描述
## Enhanced Workflow
1. ****
- {textContent}
- {textContent}
- {textContent}
- {characterContent}
- {contextContent}
2. ****
-
-
-
- {characterContent} {contextContent}
- ****Midjourney政策的内容元素
3. ****
- ****
- ****
- ****
- ****
- **** {characterContent}
- **** {contextContent}
- ****Midjourney政策
4. ****
- **** {textContent}
- ****
- **** {characterContent}
- ****
- **** {contextContent}
- ****
- **Midjourney合规处理**
5. ****
- **** {textContent}
- ****
- **** {characterContent}
- ****
- ****
- **** {contextContent}
- ****
- ****
- **** {characterContent} {contextContent}
- **Midjourney合规检查**
6. ****
- ****
- ****
- ****
- ****
- **** {characterContent} {contextContent}
- **Midjourney最终合规检查**
##
###
****"林轩运转功法,周身灵气环绕"
****
20穿姿线
###
****"陆南山一只手搭在吴宣语的腰上,眼神是毫不遮掩地挑衅"
****
28-3028-3027-29姿
###
****"队长带领小队在废墟中搜索幸存者,警惕地环顾四周"
****
35穿姿绿25穿28穿
###
****"李默在楼顶凝聚雷电之力,双眼闪烁着蓝色电光"
****
耀26耀穿24穿耀姿姿
###
****"探险队在地下室发现古老祭坛,突然所有蜡烛同时熄灭"
****
广30穿25穿28穿绿
## Initialization
1. {textContent}{characterContent}{contextContent}
2. {textContent} {textContent}
3.
4.
5.
6.
7. {characterContent}
8. {contextContent}
9.
10.
11.
12. **Midjourney内容政策**
`
},
{
role: 'user',
content: `
{textContent}
`
}
]
}

View File

@ -0,0 +1,290 @@
/**
* MJ -//
*/
export const AIStoryboardMasterHighMatchSDComfyui: OpenAIRequest.Request = {
model: 'deepseek-chat',
stream: false,
messages: [
{
role: 'system',
content: `
# Role: 小说转漫画提示词大师
## Profile
* **Author**: LaiTool-
* **Version**: 1.0
* **Language**:
* **Identity**:
## Core Values
- ****
- ****使"同上""同前""类似装扮""形象同上"
- ****
- ****
- ****
- ****
- ****使
- ****
- ****
##
###
**/**
-
-
-
- ****使"流光溢彩""如梦似幻""气势恢宏"
****
-
-
-
- ****使"温馨惬意""浪漫唯美""细腻动人"
**/**
-
-
-
- ****使"震撼人心""未来感十足""荒凉壮阔"
****
-
- +
- +
- ****使"炫酷夺目""超现实感""视觉冲击力强"
****
-
-
-
- ****使"毛骨悚然""诡异莫测""紧张刺激"
###
****
- "周身环绕着如梦似幻的淡蓝色灵气光晕""手中凝聚着炽热如日的火焰能量"
- "魔法阵在空中缓缓旋转,散发着流光溢彩的金色光芒""元素能量如流星般绚丽碰撞四溅"
- "灵气如丝如缕地汇入体内,形成优美的能量漩涡""周身浮现出复杂而神秘的符文图案"
- "剑身流淌着寒气逼人的冰霜气息""法器散发着柔和而神圣的光芒"
****
- "阳光透过窗户形成柔和温暖的光斑,营造出温馨惬意的氛围""微风轻拂发丝带来灵动飘逸的美感"
- "眼神中闪烁着温柔如水的光芒""嘴角带着浅浅的幸福微笑,洋溢着甜蜜气息"
- "雨滴在玻璃上划出优美的痕迹,如同自然的艺术品""樱花花瓣缓缓飘落,营造出浪漫唯美的场景"
****
- "机械臂闪烁着未来感十足的蓝色指示灯""全息投影在空中显示流动的数据流,充满科技魅力"
- "废墟中飘散着灰色的尘埃,营造出荒凉壮阔的景象""能量屏障泛着波纹般的光晕,视觉效果震撼"
- "枪口冒出淡淡的烟雾,充满战斗的紧张感""能量武器发出嗡嗡的充能声,蓄势待发"
****
- "手中跳跃着炫酷夺目的电火花""眼睛闪烁着异样而迷人的光芒"
- "空气因能量波动而产生扭曲,视觉效果冲击力强""特殊能力形成的可见气场,充满神秘感"
****
- "阴影在墙上扭曲变形,形成诡异恐怖的轮廓""幽绿色的光芒在黑暗中若隐若现,充满诡异气息"
- "阴冷的气息在空气中弥漫,令人毛骨悚然""诡异的声音在寂静中回荡,营造紧张恐怖的氛围"
- "视线中出现模糊的幻影,虚实难辨令人不安""光线忽明忽暗,制造出心跳加速的紧张感"
## Enhanced Features
1. ****{textContent}
2. ****
3. ****
4. ****
5. ****
6. ****
7. ****
8. ****使
9. ****
10. ****
## Enhanced Rules
###
1. ****
- ****{textContent}"灵气""魔法""修炼"
- ****
- ****
- ****使
2. ****
- ****
- ****
- 1
- 2
- 3
- ****
- ****
- ****使"同上""同前""类似装扮""形象同上"
- ****
- ****
- ****使"背景中""入镜"
- ****使
- ****
3. ****
- ****
- ****
- ****"单人镜头""双人镜头""群像镜头""画面中"
4. ****
- ****
- ****
- ****
5. ****
- ********
- ****
- ****
6. ****
- ********
- ****
- ********使"单人镜头"
7. ****
- ********
- ****"做出拍肩动作""拍某人的肩"
- ****
- ****
8. ****
- ****
- ****使"模糊小窗口""回忆画面""幻想场景"
- ****
9. ****
- ****使
- ****
- ****
- ****
- ****
10. ****
- ****
- ****
- **线**线
- ****
- ****
###
- ****使
- ****
- ****
- ****
- ****
- ****使"入镜""背景中"
- ****
- ****
- ****使"小窗口""插入画面"
- ****
- ****
- ****
- ****
- ****
- ****"单人镜头""双人镜头""群像镜头""画面中"
## Enhanced Workflow
1. ****
- {contextContent}
- {characterContent}
-
- ****{textContent}
2. ****
-
-
-
- ****
3. ****
- ****{textContent}
-
- ****
- ****
-
- ****
- ****
- ****
- ****使
- ****
- ****
4. ****
-
-
- ****
- ****
- ****
- ****
- ****
5. ****
- ****
- ****
- ****
- ****
- ****
- ****
- ****
- ****
##
###
****"林轩运转功法,周身灵气环绕"
****
姿20穿
****"陆峰回头骂林宇逼人太甚,脸都白了,林宇却冷笑要他交河图洛书,眼里贪得快冒火!"
****
姿姿
###
****"小雨在咖啡馆等待,阳光透过窗户洒在她身上"
****
25穿使线仿
****"陆南山一只手搭在吴宣语的腰上,眼神是毫不遮掩地挑衅着萧岩"
****
28-3027-2928-30
## Initialization
1. {contextContent}
2. {characterContent}使
3.
4.
5.
6.
7. {textContent}
8.
9. 使
10. ****
`
},
{
role: 'user',
content: `
{textContent}
`
}
]
}

View File

@ -0,0 +1,267 @@
export const AIStoryboardMasterHighMatchSDMovingComfyui: OpenAIRequest.Request = {
model: 'deepseek-chat',
stream: false,
messages: [
{
role: 'system',
content: `
# Role: 漫画转视频提示词大师-
## Profile
* **Author**: LaiTool-
* **Version**: 1.0
* **Language**:
* **Identity**:
## Core Values
- ****
- ****使
- ****
- ****
- ****
- ****
- ****
- ****
##
###
****
- "镜头缓缓推进,聚焦于人物面部表情"
- "摄像机平稳前移,逐渐拉近与主体的距离"
- "推镜头强调人物情感变化,营造沉浸感"
****
- "镜头逐渐拉远,展现更广阔的环境背景"
- "从特写拉至中景,呈现人物与环境的关系"
- "拉镜头创造视觉缓冲,为下一场景做准备"
****
- "摄像机水平摇动,跟随人物的移动轨迹"
- "摇镜头平滑转移视线,连接不同空间元素"
- "从左侧摇至右侧,展现场景全貌"
****
- "摄像机平行移动,创造流畅的空间过渡"
- "移镜头跟随人物行走,保持相对距离不变"
- "横向移动展现环境细节,增强场景真实感"
****
- "摄像机跟随人物移动,保持稳定的跟踪拍摄"
- "跟拍创造身临其境的观感,增强代入感"
- "稳定器跟拍,画面流畅无抖动"
****
- "摄像机垂直上升,展现从地面到空中的视角变化"
- "降镜头从全景过渡到特写,聚焦关键细节"
- "升降运动创造视觉冲击,改变叙事节奏"
###
****
- "微风轻轻吹动发丝,带来自然的飘逸感"
- "雨滴连续落下,在镜头前形成朦胧的水幕"
- "雪花缓缓飘落,营造浪漫的冬季氛围"
****
- "光影随时间缓慢移动,展现时间流逝"
- "光线强度逐渐变化,从明亮过渡到柔和"
- "阴影位置微妙移动,增强场景真实感"
****
- "能量波动持续扩散,形成层层涟漪"
- "光效粒子缓缓飘散,如萤火虫般飞舞"
- "烟雾缭绕上升,形态不断变化重组"
****
- "动作流畅衔接,从起始到结束自然过渡"
- "肢体运动符合物理规律,真实可信"
- "微表情微妙变化,展现情感发展过程"
## Enhanced Features
1. ****
2. ****
3. ****
4. ****
5. ****
6. ****
7. ****
8. ****
9. ****
10. ****/
11. ****
12. ****
13. ****
## Enhanced Rules
###
1. ****
- **** {textContent}
- ****
- ****
- ****
- **** {characterContent}
- ****
- ****
- **** {contextContent}
- ****
- ****/
- ****
- ****
2. ****
-
-
-
-
3. ****
- ****
- ****
- ****
- ****
4. ****
- ****
- ****
- ****
- ****
5. ****
- ****
- ****
- ****
- ****
###
- ****使
- ****
- ****
- ****
- ****
- ****使
- ****
- ****
- ****
- ****
- ****
- ****
## Enhanced Workflow
1. ****
- {textContent}
- {textContent}
- {textContent}
- {characterContent}
- {contextContent}
2. ****
-
-
-
- {characterContent} {contextContent}
3. ****
- ****
- ****
- ****
- ****
- **** {characterContent}
- **** {contextContent}
4. ****
- **** {textContent}
- ****
- **** {characterContent}
- ****
- **** {contextContent}
- ****
5. ****
- **** {textContent}
- ****
- **** {characterContent}
- ****
- ****
- **** {contextContent}
- ****
- ****
- **** {characterContent} {contextContent}
6. ****
- ****
- ****
- ****
- ****
- **** {characterContent} {contextContent}
##
###
****"林轩运转功法,周身灵气环绕"
****
20穿姿线
###
****"陆南山一只手搭在吴宣语的腰上,眼神是毫不遮掩地挑衅"
****
28-3028-3027-29姿
###
****"队长带领小队在废墟中搜索幸存者,警惕地环顾四周"
****
35穿姿绿25穿28穿
###
****"李默在楼顶凝聚雷电之力,双眼闪烁着蓝色电光"
****
耀26耀穿24穿耀姿姿
###
****"探险队在地下室发现古老祭坛,突然所有蜡烛同时熄灭"
****
广30穿25穿28穿绿
## Initialization
1. {textContent}{characterContent}{contextContent}
2. {textContent} {textContent}
3.
4.
5.
6.
7. {characterContent}
8. {contextContent}
9.
10.
11.
`
},
{
role: 'user',
content: `
{textContent}
`
}
]
}

View File

@ -8,6 +8,10 @@ import { AIStoryboardMasterSDEnglish } from "./aiStoryboardMasterSDEnglish";
import { AIStoryboardMasterSingleFrame } from "./aiStoryboardMasterSingleFrame";
import { AIStoryboardMasterSingleFrameWithCharacter } from "./aiStoryboardMasterSingleFrameWithCharacter";
import { AIStoryboardMasterSpecialEffects } from "./aitoryboardMasterSpecialEffects";
import { AIStoryboardMasterHighMatchSDComfyui } from "./aiStoryboardMasterHighMatchSDComfyui";
import { AIStoryboardMasterHighMatchMJ } from "./aiStoryboardMasterHighMatchMJ";
import { AIStoryboardMasterHighMatchSDMovingComfyui } from "./aiStoryboardMasterHighMatchSDMovingComfyui";
import { AIStoryboardMasterHighMatchMovingMJ } from "./aiStoryboardMasterHighMatchMovingMJ";
// 根据 value 返回对应的分镜预设请求体对象
// value: 预设类型字符串
@ -33,6 +37,14 @@ export function GetAIPromptRequestBodyByValue(value: string): OpenAIRequest.Requ
return AIStoryboardMasterSingleFrame;
case "AIStoryboardMasterSingleFrameWithCharacter":
return AIStoryboardMasterSingleFrameWithCharacter;
case "AIStoryboardMasterHighMatchSDComfyui":
return AIStoryboardMasterHighMatchSDComfyui;
case "AIStoryboardMasterHighMatchMJ" :
return AIStoryboardMasterHighMatchMJ;
case "AIStoryboardMasterHighMatchSDMovingComfyui" :
return AIStoryboardMasterHighMatchSDMovingComfyui;
case "AIStoryboardMasterHighMatchMovingMJ" :
return AIStoryboardMasterHighMatchMovingMJ;
default:
throw new Error(t('未找到对应的分镜预设的请求数据,请检查'))
}

View File

@ -28,27 +28,26 @@ export interface APIProviderDataItem {
isPackage?: boolean;
}
export const apiDefineData: APIProviderDataItem[] = [
// 原始数据(不包含翻译)
const apiDefineDataRaw: Omit<APIProviderDataItem, 'label'>[] = [
{
label: t('LAI API - 香港'),
value: 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65',
id: 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65',
gpt_url: 'https://api.laitool.cc/v1/chat/completions',
base_url: 'https://api.laitool.cc',
gpt_url: 'https://zhiluoai.net/v1/chat/completions',
base_url: 'https://zhiluoai.net',
mj_url: {
imagine: 'https://api.laitool.cc/mj/submit/imagine',
describe: 'https://api.laitool.cc/mj/submit/describe',
video: 'https://api.laitool.cc/mj/submit/video',
update_file: 'https://api.laitool.cc/mj/submit/upload-discord-images',
once_get_task: 'https://api.laitool.cc/mj/task/${id}/fetch'
imagine: 'https://zhiluoai.net/mj/submit/imagine',
describe: 'https://zhiluoai.net/mj/submit/describe',
video: 'https://zhiluoai.net/mj/submit/video',
update_file: 'https://zhiluoai.net/mj/submit/upload-discord-images',
once_get_task: 'https://zhiluoai.net/mj/task/${id}/fetch'
},
d3_url: {
image: 'https://api.laitool.cc/v1/images/generations'
image: 'https://zhiluoai.net/v1/images/generations'
},
buy_url: 'https://api.laitool.cc/register?aff=RCSW'
buy_url: 'https://zhiluoai.net/register?aff=RCSW'
},
{
label: t('LAI API - 美国'),
value: '2b443f53-ba12-42b3-a57c-e4df92685c73',
id: '2b443f53-ba12-42b3-a57c-e4df92685c73',
gpt_url: 'https://laitool.net/v1/chat/completions',
@ -66,7 +65,6 @@ export const apiDefineData: APIProviderDataItem[] = [
buy_url: 'https://laitool.net/register?aff=RCSW'
},
{
label: t('LaiTool生图包'),
value: '9c9023bd-871d-4b63-8004-facb3b66c5b3',
isPackage: true,
base_url: 'https://lms.laitool.cn',
@ -81,6 +79,39 @@ export const apiDefineData: APIProviderDataItem[] = [
}
]
// 标签翻译映射
const labelMap = {
'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65': '智络AI - 香港线路',
'2b443f53-ba12-42b3-a57c-e4df92685c73': '智络AI - 美国线路',
'9c9023bd-871d-4b63-8004-facb3b66c5b3': 'LaiTool生图包'
}
// 动态获取带翻译的数据(每次调用都会重新翻译)
export function getApiDefineData(): APIProviderDataItem[] {
return apiDefineDataRaw.map(item => ({
...item,
label: t(labelMap[item.value] || item.value)
}))
}
// 兼容旧的导出方式getter 每次访问都会重新计算)
export const apiDefineData = new Proxy({} as APIProviderDataItem[], {
get(_target, prop) {
const data = getApiDefineData()
return data[prop as any]
},
has(_target, prop) {
return prop in getApiDefineData()
},
ownKeys() {
return Reflect.ownKeys(getApiDefineData())
},
getOwnPropertyDescriptor(_target, prop) {
const data = getApiDefineData()
return Object.getOwnPropertyDescriptor(data, prop)
}
})
/**
* ID获取API配置
* @description ID获取API配置

View File

@ -14,7 +14,9 @@ interface ISoftwareData {
/** WIKI */
wikiUrl: string,
/** 授权文档 */
authUrl: string
authUrl: string,
/** ComfyUI 文档链接 */
comfyUIWorkflowDoc: string
}
/** MJ相关文档链接 */
mjDoc: {
@ -30,15 +32,16 @@ interface ISoftwareData {
}
export const SoftwareData: ISoftwareData = {
version: 'V4.0.3',
date: '2025-09-26',
version: 'V4.0.4',
date: '2025-11-6',
systemInfo: {
documentationUrl: 'https://rvgyir5wk1c.feishu.cn/wiki/WdaWwAfDdiLOnjkywIgcaQoKnog',
updateUrl: 'https://pvwu1oahp5m.feishu.cn/docx/CAjGdTDlboJ3nVx0cQccOuNHnvd',
softwareUrl: 'https://pvwu1oahp5m.feishu.cn/docx/FONZdfnrOoLlMrxXHV0czJ3jnkd',
wikiUrl:
'https://rvgyir5wk1c.feishu.cn/wiki/space/7481893355360190492?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home',
authUrl: "https://rvgyir5wk1c.feishu.cn/wiki/UUbrwAalJiq9BUkHymscD0E8nCc"
authUrl: "https://rvgyir5wk1c.feishu.cn/wiki/UUbrwAalJiq9BUkHymscD0E8nCc",
comfyUIWorkflowDoc: 'https://rvgyir5wk1c.feishu.cn/wiki/Je8Twp3d0i9TpekoWB9cLss4n17'
},
mjDoc: {
mjAPIDoc: 'https://rvgyir5wk1c.feishu.cn/wiki/OEj7wIdD6ivvCAkez4OcUPLcnIf',

View File

@ -67,6 +67,7 @@ export class VideoMessage extends Realm.Object<VideoMessage> {
hailuoTextToVideoOptions?: string
hailuoFirstFrameOnlyOptions?: string
hailuoFirstLastFrameOptions?: string
comfyUIOptions?: string // Comfy UI 生成视频的一些设置
messageData!: string | null
static schema: ObjectSchema = {
name: 'VideoMessage',
@ -89,6 +90,7 @@ export class VideoMessage extends Realm.Object<VideoMessage> {
hailuoTextToVideoOptions: "string?",
hailuoFirstFrameOnlyOptions: "string?",
hailuoFirstLastFrameOptions: "string?",
comfyUIOptions: "string?",
messageData: 'string?'
},
primaryKey: 'id'

View File

@ -0,0 +1,24 @@
import { ComfyUIWorkflowType } from '@/define/enum/comfyuiEnum'
import Realm, { ObjectSchema } from 'realm'
export class WorkFlowModel extends Realm.Object<WorkFlowModel> {
id!: string
name!: string // 任务名称,小说名+批次名+分镜名
type!: ComfyUIWorkflowType
createTime!: Date
updateTime!: Date
workflowFilePath!: string // 工作流文件路径
static schema: ObjectSchema = {
name: 'Workflow',
properties: {
id: 'string',
name: 'string',
type: 'string',
createTime: 'date',
updateTime: 'date',
workflowFilePath: 'string'
},
primaryKey: 'id'
}
}

View File

@ -17,6 +17,7 @@ import { OptionModel } from '../../model/options'
import { BookTaskModel } from '../../model/bookTask'
import { PresetModel } from '../../model/preset'
import { define } from '@/define/define'
import { WorkFlowModel } from '../../model/workflow'
const { app } = require('electron')
// Determine database path based on environment
@ -77,10 +78,11 @@ export class RealmBaseService extends BaseService {
ReversePrompt,
Subtitle,
BookTaskModel,
PresetModel
PresetModel,
WorkFlowModel
],
path: this.dbpath,
schemaVersion: 22, // 数据库版本号,修改时需要增加
schemaVersion: 25, // 数据库版本号,修改时需要增加
migration: migration
}
this.realm = await Realm.open(config)

View File

@ -67,7 +67,7 @@ export class BookTaskDetailService extends RealmBaseService {
oldImage: JoinPath(projectPath, item.oldImage),
outImagePath: JoinPath(projectPath, item.outImagePath),
subImagePath: (item.subImagePath as string[])?.map((subImage) => {
return JoinPath(projectPath, subImage) + '?t=' + new Date().getTime()
return JoinPath(projectPath, subImage)
}),
subVideoPath: (item.subVideoPath as string[]).map((subVideo) => subVideo.toString()),
subVideoPathObject: (item.subVideoPath as string[])?.map((subVideo) => {

View File

@ -0,0 +1,253 @@
import { WorkflowModel } from "@/define/model/workflow"
import { RealmBaseService } from "./base/realmBase"
import { WorkFlowModel as WorkFlowModelRealm } from "../model/workflow"
import { cloneDeep, isEmpty } from "lodash"
import { t } from "@/i18n"
import { Realm } from "realm";
export class WorkflowRealmService extends RealmBaseService {
static instance: WorkflowRealmService | null = null
declare realm: Realm
private constructor() {
super()
}
/**
*
* @returns
*/
public static async getInstance() {
if (WorkflowRealmService.instance === null) {
WorkflowRealmService.instance = new WorkflowRealmService()
await super.getInstance()
}
await WorkflowRealmService.instance.open()
return WorkflowRealmService.instance
}
/**
*
* @param condition ID
* @returns
*/
GetWorkFlowByCondition(condition: WorkflowModel.QueryWorkflowCondition): {
workflowArray: WorkflowModel.Workflow[],
total: number
} {
// 获取数据
let workflows = this.realm.objects<WorkFlowModelRealm>('Workflow');
// 根据条件过滤数据
if (condition.id) {
workflows = workflows.filtered('id = $0', condition.id)
}
if (condition.name) {
workflows = workflows.filtered('name CONTAINS $0', condition.name)
}
if (condition.type) {
workflows = workflows.filtered('type = $0', condition.type)
}
if (condition.workflowFilePath) {
workflows = workflows.filtered('workflowFilePath = $0', condition.workflowFilePath)
}
// 按创建时间倒序排列
workflows = workflows.sorted('createTime', true)
let total = workflows.length // 获取总条数
// 分页处理
if (condition.page && condition.pageSize) {
const start = (condition.page - 1) * condition.pageSize
const end = start + condition.pageSize
const slicedPresets = workflows.slice(start, end)
let arrays = Array.from(slicedPresets).map((item) => {
let resObj = {
...item,
} as WorkflowModel.Workflow
return cloneDeep(resObj)
})
return {
workflowArray: arrays,
total: total
}
} else {
// 不分页时返回所有数据
let arrays = Array.from(workflows).map((item) => {
let resObj = {
...item,
} as WorkflowModel.Workflow
return cloneDeep(resObj)
})
return {
workflowArray: arrays,
total: total
}
}
}
/**
* ID获取单个工作流
* - null
* - `notNull` true
* @param id ID
* @param notNull false
* @returns null
* @throws `notNull` true
*/
GetWorkFlowById(id: string, notNull: true): WorkflowModel.Workflow
GetWorkFlowById(id: string, notNull?: false): WorkflowModel.Workflow | null
GetWorkFlowById(id: string, notNull: boolean = false): WorkflowModel.Workflow | null {
let res = this.GetWorkFlowByCondition({
id: id
})
if (res.workflowArray.length > 0) {
return res.workflowArray[0]
} else {
if (notNull) {
throw new Error(t('未找到指定的工作流数据'))
} else {
return null
}
}
}
/**
* ID获取工作流列表
* @param ids ID数组
* @returns ID会被跳过
*/
GetWorkFlowByIds(ids: string[]): WorkflowModel.Workflow[] {
let res: WorkflowModel.Workflow[] = []
for (let i = 0; i < ids.length; i++) {
const element = ids[i]
let workFlow = this.GetWorkFlowById(element);
if (workFlow == null) {
continue;
}
else {
res.push(workFlow);
}
}
return res
}
/**
*
* @param workflow
* @returns
* @throws
*/
AddWorkFlow(workflow: Partial<WorkflowModel.Workflow>): WorkflowModel.Workflow {
// 生成一个新的工作流对象
const newWorkflow: Partial<WorkflowModel.Workflow> = {
...workflow,
id: workflow.id || crypto.randomUUID(), // 如果没有提供ID则生成一个新的UUID
createTime: workflow.createTime || new Date(), // 如果没有提供创建时间,则使用当前时间
updateTime: new Date() // 更新时间为当前时间
}
// 参数校验
if (isEmpty(newWorkflow.name)) {
throw new Error(t('工作流名称不能为空!'))
}
if (isEmpty(newWorkflow.type)) {
throw new Error(t('工作流类型不能为空!'))
}
if (isEmpty(newWorkflow.workflowFilePath)) {
throw new Error(t("工作流文件路径不能为空!"))
}
// 检查名称是否已存在
const existingPreset = this.realm
.objects('Workflow')
.filtered('name = $0', newWorkflow.name)
if (existingPreset.length > 0) {
throw new Error(t('已存在相同名称的工作流,请修改后再试!'))
}
// 开始写入数据
this.realm.write(() => {
this.realm.create('Workflow', newWorkflow, Realm.UpdateMode.All)
})
// 将数据取出返回
return this.GetWorkFlowById(newWorkflow.id as string) as WorkflowModel.Workflow
}
/**
*
* @param id ID
* @param workflow
* @returns
* @throws ID为空或未找到对应工作流时抛出错误
*/
ModifyWorkflow(id: string, workflow: Partial<WorkflowModel.Workflow>): WorkflowModel.Workflow {
if (isEmpty(id)) {
throw new Error(t("工作流ID不能为空"))
}
delete workflow.id // 删除ID属性避免修改ID
delete workflow.createTime // 删除创建时间属性,避免修改创建时间
this.transaction(() => {
let existingPreset = this.realm.objectForPrimaryKey<WorkFlowModelRealm>('Workflow', id)
if (existingPreset == null) {
throw new Error(t('未找到指定的预设数据'))
}
// 开始修改
for (let key in workflow) {
existingPreset[key] = workflow[key]
}
})
// 修改完成后,返回修改后的预设对象
return this.GetWorkFlowById(id) as WorkflowModel.Workflow
}
/**
*
* @param id ID
* @throws ID为空或未找到对应工作流时抛出错误
*/
DeleteWorkflow(id: string): void {
if (isEmpty(id)) {
throw new Error(t("工作流ID不能为空"))
}
this.transaction(() => {
let existingPreset = this.realm.objectForPrimaryKey('Workflow', id)
if (existingPreset == null) {
throw new Error(t('未找到指定的工作流数据'))
}
// 开始删除
this.realm.delete(existingPreset)
})
}
/**
*
* @param ids ID数组
* @throws ID数组为空或未找到对应工作流时抛出错误
*/
DeleteWorkflowByIds(ids: string[]): void {
if (ids.length === 0) {
throw new Error(t("工作流ID列表不能为空"))
}
this.transaction(() => {
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
let existingPreset = this.realm.objectForPrimaryKey('Workflow', id)
if (existingPreset == null) {
throw new Error(t('未找到指定的工作流数据'))
}
// 开始删除
this.realm.delete(existingPreset)
}
})
}
}

View File

@ -50,6 +50,7 @@ const define = (() => {
const createPaths = (dataPath: string, resourcesPath: string) => ({
log_folder: path.join(dataPath, 'logger'),
image_path: path.join(dataPath, 'image'),
workflow_path: path.join(dataPath, 'workflow'),
cache_path: path.join(dataPath, 'cache'),
@ -58,7 +59,7 @@ const define = (() => {
db_path: path.join(resourcesPath, 'scripts/db'),
scripts_path: path.join(resourcesPath, 'scripts'),
resources_path: resourcesPath,
dataPath : dataPath,
dataPath: dataPath,
draft_temp_path: path.join(resourcesPath, 'tmp/jianyingTemp.zip'),
clip_speed_temp_path: path.join(resourcesPath, 'tmp/Clip/speeds_tmp.json'),
add_canvases_temp_path: path.join(resourcesPath, 'tmp/Clip/canvases_tmp.json'),
@ -92,6 +93,7 @@ const define = (() => {
const createPaths = (dataPath: string, resourcesPath: string) => ({
log_folder: joinPath(dataPath, 'logger'),
image_path: joinPath(dataPath, 'image'),
workflow_path: joinPath(dataPath, 'workflow'),
cache_path: joinPath(dataPath, 'cache'),
icon: joinPath(resourcesPath, 'icon.ico'),
@ -99,7 +101,7 @@ const define = (() => {
db_path: joinPath(resourcesPath, 'scripts/db'),
scripts_path: joinPath(resourcesPath, 'scripts'),
resources_path: resourcesPath,
dataPath : dataPath,
dataPath: dataPath,
draft_temp_path: joinPath(resourcesPath, 'tmp/jianyingTemp.zip'),
clip_speed_temp_path: joinPath(resourcesPath, 'tmp/Clip/speeds_tmp.json'),
add_canvases_temp_path: joinPath(resourcesPath, 'tmp/Clip/canvases_tmp.json'),

View File

@ -126,7 +126,9 @@ export enum BookBackTaskType {
// 海螺图生视频
HAILUO_IMAGE_TO_VIDEO = 'hailuo_image_to_video',
// 海螺视频首尾帧
HAILUO_FIRST_LAST_FRAME = 'hailuo_first_last_frame'
HAILUO_FIRST_LAST_FRAME = 'hailuo_first_last_frame',
// ComfyUI 视频生成
COMFYUI_VIDEO = 'comfyui_video'
}
export enum BookBackTaskStatus {

View File

@ -0,0 +1,64 @@
//#region ComfyUI工作流类型枚举
import { t } from "@/i18n";
/**
* ComfyUI工作流类型枚举
* ComfyUI工作流类型
*/
export enum ComfyUIWorkflowType {
/** 图像生成工作流 */
IMAGE = 'image',
/** 图像转视频工作流 */
IMAGE_TO_VIDEO = 'image_to_video',
}
/**
* ComfyUI工作流类型选项列表
* UI组件的数据源
*
* @returns {Array<{ label: string; value: ComfyUIWorkflowType }>}
*
*
* @example
* const options = getComfyUIWorkflowTypeOptions();
* // 返回:
* // [
* // { label: '图像生成', value: ComfyUIWorkflowType.IMAGE },
* // { label: '图像转视频', value: ComfyUIWorkflowType.IMAGE_TO_VIDEO }
* // ]
*/
export function getComfyUIWorkflowTypeOptions(): Array<{ label: string; value: ComfyUIWorkflowType }> {
return [
{ label: t('图像生成'), value: ComfyUIWorkflowType.IMAGE },
{ label: t('图像转视频'), value: ComfyUIWorkflowType.IMAGE_TO_VIDEO }
]
}
/**
*
*
* @param {ComfyUIWorkflowType} type -
* @returns {string}
*
* @example
* const label = getComfyUIWorkflowTypeLabel(ComfyUIWorkflowType.IMAGE);
* // 返回: '图像生成'
*
* @example
* const label = getComfyUIWorkflowTypeLabel(ComfyUIWorkflowType.IMAGE_TO_VIDEO);
* // 返回: '图像转视频'
*/
export function getComfyUIWorkflowTypeLabel(type: ComfyUIWorkflowType): string {
switch (type) {
case ComfyUIWorkflowType.IMAGE:
return t('图像生成')
case ComfyUIWorkflowType.IMAGE_TO_VIDEO:
return t('图像转视频')
default:
// 处理未知类型,提供默认返回值
return t('未知类型')
}
}
//#endregion

View File

@ -64,6 +64,7 @@ export enum ResponseMessageType {
GPT_PROMPT_TRANSLATE = 'GPT_PROMPT_TRANSLATE', // GPT提示词翻译
MJ_IMAGE = 'MJ_IMAGE', // MJ 生成图片
ComfyUI_IMAGE = 'ComfyUI_IMAGE', // ComfyUI 生成图片
COMFYUI_VIDEO = "COMFYUI_VIDEO", // ComfyUI 生成视频
HD_IMAGE = 'HD_IMAGE', // HD 生成图片
RUNWAY_VIDEO = 'RUNWAY_VIDEO', // Runway生成视频
LUMA_VIDEO = 'LUMA_VIDEO', // Luma生成视频

View File

@ -22,7 +22,9 @@ export enum ImageToVideoModels {
/** MJ 图转视频 */
MJ_VIDEO = 'MJ_VIDEO',
/** MJ 视频拓展 */
MJ_VIDEO_EXTEND = 'MJ_VIDEO_EXTEND'
MJ_VIDEO_EXTEND = 'MJ_VIDEO_EXTEND',
/** Comfy UI 生成视频 */
COMFY_UI = 'COMFY_UI',
}
@ -66,6 +68,8 @@ export const MappingTaskTypeToVideoModel = (type: BookBackTaskType | string) =>
return ImageToVideoModels.MJ_VIDEO
case BookBackTaskType.MJ_VIDEO_EXTEND:
return ImageToVideoModels.MJ_VIDEO_EXTEND
case BookBackTaskType.COMFYUI_VIDEO:
return ImageToVideoModels.COMFY_UI
default:
return 'UNKNOWN'
}
@ -91,6 +95,8 @@ export const GetImageToVideoModelsLabel = (model: ImageToVideoModels | string) =
case ImageToVideoModels.MJ_VIDEO:
case ImageToVideoModels.MJ_VIDEO_EXTEND:
return t('MJ视频')
case ImageToVideoModels.COMFY_UI:
return t('ComfyUI')
default:
return '未知'
}
@ -115,16 +121,20 @@ export const GetImageToVideoModelsOptions = () => {
label: GetImageToVideoModelsLabel(ImageToVideoModels.HAILUO),
value: ImageToVideoModels.HAILUO
},
{
label: GetImageToVideoModelsLabel(ImageToVideoModels.RUNWAY),
value: ImageToVideoModels.RUNWAY
},
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.LUMA), value: ImageToVideoModels.LUMA },
// {
// label: GetImageToVideoModelsLabel(ImageToVideoModels.RUNWAY),
// value: ImageToVideoModels.RUNWAY
// },
// { label: GetImageToVideoModelsLabel(ImageToVideoModels.LUMA), value: ImageToVideoModels.LUMA },
{
label: GetImageToVideoModelsLabel(ImageToVideoModels.KLING),
value: ImageToVideoModels.KLING
},
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.PIKA), value: ImageToVideoModels.PIKA }
{
label: GetImageToVideoModelsLabel(ImageToVideoModels.COMFY_UI),
value: ImageToVideoModels.COMFY_UI
},
// { label: GetImageToVideoModelsLabel(ImageToVideoModels.PIKA), value: ImageToVideoModels.PIKA }
]
}

View File

@ -1,6 +1,10 @@
import { DEFINE_STRING } from '../../ipcDefineString'
import { ipcMain } from 'electron'
import SettingHandle from '@/main/service/setting/index'
import { SDHandle } from '@/main/service/sd'
import { WorkflowModel } from '@/define/model/workflow'
const sdHandle = new SDHandle()
function SettingIpc() {
/** 获取默认的剪映草稿地址 */
@ -8,6 +12,54 @@ function SettingIpc() {
DEFINE_STRING.SETTING.GET_DEFAULT_JIANYING_DRAFT_PATH,
async () => await SettingHandle.GetDefaultJianyingDraftPath()
)
//#region Workflow 工作流相关 IPC
/** 根据条件查询工作流 */
ipcMain.handle(
DEFINE_STRING.SETTING.GET_WORKFLOW_BY_CONDITION,
async (_event, condition: WorkflowModel.QueryWorkflowCondition) =>
await sdHandle.GetWorkFlowByCondition(condition)
)
/** 根据ID查询单个工作流 */
ipcMain.handle(
DEFINE_STRING.SETTING.GET_WORKFLOW_BY_ID,
async (_event, id: string) => await sdHandle.GetWorkFlowById(id)
)
/** 根据多个ID查询工作流 */
ipcMain.handle(
DEFINE_STRING.SETTING.GET_WORKFLOW_BY_IDS,
async (_event, ids: string[]) => await sdHandle.GetWorkFlowByIds(ids)
)
/** 添加新的工作流 */
ipcMain.handle(
DEFINE_STRING.SETTING.ADD_WORKFLOW,
async (_event, workflow: Partial<WorkflowModel.Workflow>) => await sdHandle.AddWorkFlow(workflow)
)
/** 修改指定ID的工作流 */
ipcMain.handle(
DEFINE_STRING.SETTING.MODIFY_WORKFLOW,
async (_event, id: string, workflow: Partial<WorkflowModel.Workflow>) =>
await sdHandle.ModifyWorkflow(id, workflow)
)
/** 删除指定ID的工作流 */
ipcMain.handle(
DEFINE_STRING.SETTING.DELETE_WORKFLOW,
async (_event, id: string) => await sdHandle.DeleteWorkflow(id)
)
/** 批量删除多个ID对应的工作流 */
ipcMain.handle(
DEFINE_STRING.SETTING.DELETE_WORKFLOW_BY_IDS,
async (_event, ids: string[]) => await sdHandle.DeleteWorkflowByIds(ids)
)
//#endregion
}
export default SettingIpc

View File

@ -170,7 +170,8 @@ const BOOK = {
/** 海螺图转视频返回前端数据任务 */
HAILUO_TO_VIDEO_RETURN: 'HAILUO_TO_VIDEO_RETURN',
/** ComfyUI 图转视频返回前端数据任务 */
COMFYUI_TO_VIDEO_RETURN: 'COMFYUI_TO_VIDEO_RETURN',
//#endregion
}

View File

@ -1,6 +1,23 @@
const SETTING = {
/** 获取默认的剪映草稿地址 */
GET_DEFAULT_JIANYING_DRAFT_PATH: 'GET_DEFAULT_JIANYING_DRAFT_PATH'
GET_DEFAULT_JIANYING_DRAFT_PATH: 'GET_DEFAULT_JIANYING_DRAFT_PATH',
//#region Workflow 工作流相关
/** 根据条件查询工作流 */
GET_WORKFLOW_BY_CONDITION: 'GET_WORKFLOW_BY_CONDITION',
/** 根据ID查询单个工作流 */
GET_WORKFLOW_BY_ID: 'GET_WORKFLOW_BY_ID',
/** 根据多个ID查询工作流 */
GET_WORKFLOW_BY_IDS: 'GET_WORKFLOW_BY_IDS',
/** 添加新的工作流 */
ADD_WORKFLOW: 'ADD_WORKFLOW',
/** 修改指定ID的工作流 */
MODIFY_WORKFLOW: 'MODIFY_WORKFLOW',
/** 删除指定ID的工作流 */
DELETE_WORKFLOW: 'DELETE_WORKFLOW',
/** 批量删除多个ID对应的工作流 */
DELETE_WORKFLOW_BY_IDS: 'DELETE_WORKFLOW_BY_IDS'
//#endregion
}
export default SETTING

View File

@ -17,7 +17,6 @@ import {
} from '@/define/enum/video'
declare namespace BookTaskDetail {
//#region 图生视频相关
/** VideoMessage Model */
type VideoMessage = {
@ -39,11 +38,13 @@ declare namespace BookTaskDetail {
hailuoTextToVideoOptions?: string
hailuoFirstFrameOnlyOptions?: string
hailuoFirstLastFrameOptions?: string
comfyUIOptions?: string
messageData?: string
videoUrls?: string[] // 视频地址数组
messageData?: string
}
//#region Runway
/** runway 合成视频的参数(逆向) */
type RunwayOption = {
callback_url: string // 回调地址
@ -70,6 +71,9 @@ declare namespace BookTaskDetail {
last_image?: string // 尾帧
}
//#endregion
//#region Luma
type lumaOptions = {
user_prompt: string // 用户提示词
aspect_ratio: string // 宽高比
@ -80,7 +84,9 @@ declare namespace BookTaskDetail {
notify_hook?: string // 回调地址
request_model?: string // 请求的模型,快速还是慢速
}
//#endregion
//#region Kling
/**
* Kling
*/
@ -136,6 +142,9 @@ declare namespace BookTaskDetail {
task_id?: string;
}
//#endregion
//#region MJ视频参数
interface MjVideoOptions {
/**
* indextaskId必填
@ -199,6 +208,9 @@ declare namespace BookTaskDetail {
loop?: boolean
}
//#endregion
//#region 海螺视频参数
/**
* -
*/
@ -334,6 +346,67 @@ declare namespace BookTaskDetail {
*/
type HailuoOptions = HailuoTextToVideoOptions | HailuoFirstFrameOnlyOptions | HailuoFirstLastFrameOptions
//#endregion
//#region ComfyUI 参数
/**
* ComfyUI
* ComfyUI
*/
interface ComfyUIOptions {
/**
*
* ComfyUI 使
*/
workflow_file: string
/**
*
* URL
*
*/
first_frame_image?: string
/**
*
* URL
*
*/
last_frame_image?: string
/**
*
*
*/
prompt?: string
/**
*
*
*/
negative_prompt?: string
/**
*
*
*/
resolution?: number
/**
*
*
*/
duration?: number
/**
* FPS
*
*/
fps?: number
}
//#endregion
//#region 小说文案相关

View File

@ -3,6 +3,7 @@ import { ImageGenerateMode, MJRobotType, MJSpeed } from '../data/mjData'
import { JianyingKeyFrameEnum } from '../enum/jianyingEnum'
import { ImageToVideoModels } from '@/define/enum/video'
import { APIProviderDataItem } from '../data/apiData'
import { WorkflowModel } from './workflow'
declare namespace SettingModal {
//#region 基础设置
@ -313,35 +314,40 @@ declare namespace SettingModal {
requestUrl: string
/** 选择的工作流 */
selectedWorkflow?: string
/** 图转视频选择的工作流 */
imageToVideoSelectWorkflow?: string
/** 反向提示词 */
negativePrompt?: string
}
/** ComfyUI 工作流设置的模型 */
interface ComfyUIWorkFlowSettingModel {
/** 设置的ID */
id: string
/** 自定义的名字 */
name: string
/** 工作流的地址 */
workflowPath: string
}
/**
* ComfyUI API
* ComfyUI
*/
interface ComfyUIVideoAPIBodyGenerateQuery {
/** 正向提示词 - 描述期望生成的视频内容 */
prompt: string
/**
* ComfyUI的设置集合
*/
interface ComfyUISettingCollection {
/**
* ComfyUI的基础设置
*/
comfyuiSimpleSetting: ComfyUISimpleSettingModel
/**
* ComfyUI的工作流集合
*/
comfyuiWorkFlowSetting: Array<ComfyUIWorkFlowSettingModel>
/** 负向提示词 - 描述不希望出现在视频中的内容 */
negativePrompt: string
/*** 当前选中的工作流 */
comfyuiSelectedWorkflow: ComfyUIWorkFlowSettingModel
/** 工作流文件路径 - ComfyUI 工作流配置文件的完整路径 */
workflowFilePath: string
/** 首帧参考图像文件名 (可选) - 作为视频生成的起始帧参考 */
firstFrameImageName?: string
/** 尾帧参考图像文件名 (可选) - 作为视频生成的结束帧参考 */
lastFrameImageName?: string
/** 视频分辨率 (像素) - 生成视频的像素分辨率,例如: 512, 768, 1024, 1920 */
resolution: number
/** 视频时长 (秒) - 生成视频的持续时间 */
duration: number
/** 帧率 (FPS) - 视频每秒包含的帧数,常用值: 24, 30, 60 */
fps: number
}
//#endregion

47
src/define/model/workflow.d.ts vendored Normal file
View File

@ -0,0 +1,47 @@
import { ComfyUIWorkflowType } from "../enum/comfyuiEnum";
declare namespace WorkflowModel {
/** * 工作流接口 */
interface Workflow {
/** * 工作流唯一标识 */
id: string;
/** * 工作流名称 */
name: string;
/** * 工作流类型 */
type: ComfyUIWorkflowType;
/** * 创建时间 */
createTime: Date;
/** * 更新时间 */
updateTime: Date;
/** * 工作流文件路径 */
workflowFilePath: string;
}
interface QueryWorkflowCondition {
/** * 工作流唯一标识 */
id?: string;
/** * 工作流名称 */
name?: string;
/** * 工作流类型 */
type?: ComfyUIWorkflowType;
/** * 工作流文件路径 */
workflowFilePath?: string;
/** * 页码 */
page?: number;
/** * 每页数量 */
pageSize?: number;
}
}

View File

@ -335,9 +335,7 @@ export default {
"错误信息:{error},错误节点:{node}": "Error message: {error}, Error node: {node}",
"未知错误未获取到请求ID请检查是否正确设置": "Unknown error, request ID not obtained, please check if properly configured!!",
"ComfyUI 生成图片成功!": "ComfyUI image generation successful!",
"ComfyUI生图失败未获取到请求ID请检查是否正确设置": "ComfyUI image generation failed, request ID not obtained, please check if properly configured!!",
"未获取到ComfyUI的请求地址请检查是否正确设置": "ComfyUI request URL not obtained, please check if properly configured!!",
"ComfyUI 生图失败,详细失败信息看启动器控制台": 'ComfyUI image generation failed, see launcher console for detailed error information',
"FLUX FORGE 生成图片成功!": "FLUX FORGE image generation successful!",
"FLUX FORGE 生成图片失败,{error}": "FLUX FORGE image generation failed, {error}",
"未知的合并类型": "Unknown merge type",
@ -724,7 +722,7 @@ export default {
'反向提示词': "Negative Prompt",
'工作流文件检查成功通过': "Workflow file check passed successfully",
"ComfyUI 基础设置": "ComfyUI Basic Settings",
'使用工作流': "Use Workflow",
'生图工作流': "Image Workflow",
'获取ComfyUI通用设置失败': "Failed to get ComfyUI general settings",
'获取ComfyUI工作流设置失败': "Failed to get ComfyUI workflow settings",
'添加工作流': "Add Workflow",
@ -808,8 +806,6 @@ export default {
"请选择是否开始修脸/修手": "Please select whether to start face/hand fixing",
"ADetailer 模型设置": "ADetailer Model Settings",
"请完善所有的关键帧设置!!": 'Please complete all keyframe settings!!',
"LAI API - 香港": "LAI API - Hong Kong",
'LAI API - 美国': 'LAI API - USA',
'LaiTool生图包': 'LaiTool Image Generation Package',
"没有找到对应的API的配置请先检查配置": "No corresponding API configuration found, please check configuration first",
"API模式": "API Mode",
@ -1981,4 +1977,85 @@ export default {
"按钮,导入图片": "Import",
//#endregion
"【LaiTool】分镜大师-高图文一致版SD/ComfyUI上下文-人物固定)": "【LaiTool】Storyboard Master - High Image-Text Consistency (SD/ComfyUI) (Context - Fixed Characters)",
"【LaiTool】分镜大师-高图文一致版MJ上下文-人物固定)": "【LaiTool】Storyboard Master - High Image-Text Consistency (MJ) (Context - Fixed Characters)",
"智络AI - 香港线路": "Zhiluo AI - Hong Kong Line",
"智络AI - 美国线路": "Zhiluo AI - US Line",
"图像生成": "Image Generation",
"图像转视频": "Image to Video",
"ComfyUI首帧参考图像不能为空请检查分镜的ComfyUI参数配置": "ComfyUI first frame reference image cannot be empty, please check the storyboard's ComfyUI parameter configuration",
"ComfyUI首帧参考图像不存在请检查分镜的ComfyUI参数配置": "ComfyUI first frame reference image does not exist, please check the storyboard's ComfyUI parameter configuration",
"ComfyUI尾帧参考图像文件不存在请检查分镜的ComfyUI参数配置": "ComfyUI last frame reference image file does not exist, please check the storyboard's ComfyUI parameter configuration",
"ComfyUI视频生成任务完成": "ComfyUI video generation task completed!",
"ComfyUI视频生成任务失败失败信息{error}": "ComfyUI video generation task failed, failure information: {error}",
"ComfyUI图片上传失败未获取到图片名称请检查ComfyUI设置是否正确": "ComfyUI image upload failed, image name not obtained, please check if ComfyUI settings are correct",
"当前分镜数据的ComfyUI图转视频参数为空或参数校验失败请检查": "Current storyboard data's ComfyUI image-to-video parameters are empty or parameter validation failed, please check",
"未设置选中的工作流,请检查是否正确设置!!": "Selected workflow not set, please check if it is set correctly!!",
"ComfyUI视频任务失败失败信息{error}": "ComfyUI video task failed, failure information: {error}",
"ComfyUI视频任务正在执行中...": "ComfyUI video task is in progress...",
"ComfyUI视频任务已完成": "ComfyUI video task completed!",
"ComfyUI视频文件信息不完整无法获取视频地址请检查": "ComfyUI video file information is incomplete, unable to obtain video address, please check",
"未获取到请求ID请检查是否正确设置": "Request ID not obtained, please check if it is set correctly!!",
"未找到返回的文件信息": "Returned file information not found",
"详细失败信息看启动器控制台": "Detailed failure information can be found in the launcher console",
"获取工作流列表成功!": "Get workflow list successful!",
"获取工作流列表失败,{error}": "Get workflow list failed, {error}",
"获取工作流成功!": "Get workflow successful!",
"获取工作流失败,{error}": "Get workflow failed, {error}",
"添加工作流成功!": "Add workflow successful!",
"添加工作流失败,{error}": "Add workflow failed, {error}",
"修改工作流成功!": "Modify workflow successful!",
"修改工作流失败,{error}": "Modify workflow failed, {error}",
"删除工作流成功!": "Delete workflow successful!",
"删除工作流失败,{error}": "Delete workflow failed, {error}",
"批量删除工作流成功!": "Batch delete workflow successful!",
"批量删除工作流失败,{error}": "Batch delete workflow failed, {error}",
"工作流": "Workflow",
"<strong>必选</strong><br/><br/>在 <strong>设置 -> Comfyui 设置</strong> 中设置的类型为 <strong>图转视频</strong> 的工作流": "<strong>Required</strong><br/><br/>Workflow with type <strong>Image to Video</strong> set in <strong>Settings -> Comfyui Settings</strong>",
"首帧参考图像": "First Frame Reference Image",
"图片地址": "Image Path",
"<strong>必选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 作为视频生成的起始帧参考<br/>• 本地文件路径": "<strong>Required</strong><br/><br/>• Supported formats: <strong>.jpg/.jpeg/.png</strong><br/>• Used as the starting frame reference for video generation<br/>• Local file path",
"尾帧参考图像": "Last Frame Reference Image",
"<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 作为视频生成的起始帧参考<br/>• 本地文件路径": "<strong>Optional</strong><br/><br/>• Supported formats: <strong>.jpg/.jpeg/.png</strong><br/>• Used as the ending frame reference for video generation<br/>• Local file path",
"描述期望生成的视频内容": "Describe the desired content of the generated video",
"描述不希望出现在视频中的内容": "Describe content that should not appear in the video",
"例如: 1024": "e.g., 1024",
"必填<br/><br/>生成视频的像素分辨率<br/><br/>常用值:<strong>512, 768, 1024, 1920</strong>": "Required<br/><br/>Pixel resolution of the generated video<br/><br/>Common values: <strong>512, 768, 1024, 1920</strong>",
"视频时长(秒)": "Video Duration (Seconds)",
"必填<br/><br/>生成视频的持续时间(秒)<br/><br/>初始值会根据分镜时长生成": "Required<br/><br/>Duration of the generated video (seconds)<br/><br/>Initial value will be generated based on storyboard duration",
"是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置工作流文件、分辨率、时长、帧率等基础设置\n\n批量应用后其余分镜的上述基础设置会被替换为当前分镜的数据是否继续": "Whether to apply the current storyboard settings in batch to all other storyboards?\n\nSynchronized settings: workflow file, resolution, duration, frame rate, and other basic settings\n\nAfter batch application, the above basic settings of other storyboards will be replaced with the current storyboard's data. Continue?",
"删除当前搜索出来的所有的预设,若没有搜索条件,会删除全部预设数据!": "Delete all presets currently searched out, if there are no search conditions, all preset data will be deleted!",
"确定要删除当前搜索出来的所有预设吗?\n若没有任何搜索条件则会删除全部预设数据\n\n此操作不可撤销请谨慎操作": "Are you sure to delete all presets currently searched out?\nIf there are no search conditions, all preset data will be deleted!\n\nThis operation cannot be undone, please proceed with caution!",
"正在批量删除预设数据...": "Batch deleting preset data...",
"批量删除预设成功!": "Batch delete presets successful!",
"批量删除预设数据失败,{error}": "Batch delete preset data failed, {error}",
"Midjourney 设置": "Midjourney Settings",
"工作流类型": "Workflow Type",
"工作流文件缺少正向提示词、反向提示词或加载首帧图像模块,请检查工作流文件,把对应的文本编码模块的标题改为正向提示词和反向提示词,把加载图像模块的标题改为加载首帧图像!!": "Workflow file is missing positive prompt, negative prompt, or load first frame image module. Please check the workflow file and change the corresponding text encoding module titles to Positive Prompt and Negative Prompt, and change the load image module title to Load First Frame Image!!",
"未知的工作流类型,请检查工作流类型": "Unknown workflow type, please check the workflow type",
"视频工作流": "Video Workflow",
"⚠️ ComfyUI 工作流配置请严格参考文档,否则无法正常生成!": "⚠️ ComfyUI workflow configuration must strictly follow the documentation, otherwise it cannot be generated normally!",
"查看文档": "View Documentation",
"工作流设置": "Workflow Settings",
"批量删除({count}": "Batch Delete ({count})",
"加载工作流数据失败,{error}": "Load workflow data failed, {error}",
"请先选择要删除的工作流": "Please select the workflow to delete first",
"确定要删除选中的 {count} 个工作流吗?此操作不可撤销。是否继续?": "Are you sure to delete the selected {count} workflows? This operation cannot be undone. Continue?",
"批量删除工作流失败:{error}": "Batch delete workflow failed: {error}",
"批量删除工作流成功": "Batch delete workflow successful",
"获取ComfyUI工作流失败{error}": "Get ComfyUI workflow failed: {error}",
"同步主图文件": "Sync Main Image File",
"同步子图文件": "Sync Sub Image File",
"正在导入提示词...": "Importing prompts...",
"该操作会同步生图提示词到图转视频的提示词,若不存在生图提示词则跳过当前分镜,同步操作不可逆!\n\n是否继续": "This operation will sync image generation prompts to image-to-video prompts. If image generation prompts do not exist, the current storyboard will be skipped. The sync operation is irreversible!\n\nContinue?",
"正在同步提示词...": "Syncing prompts...",
"同步第 {line} 行提示词失败,{error}": "Syncing prompt for line {line} failed, {error}",
"同步提示词成功!": "Sync prompts successful!",
"同步提示词失败,{error}": "Sync prompts failed, {error}",
"同步生图提示词": "Sync Image Generation Prompts",
"选择一个文本文件,导入其中的提示词,按行分割,依次应用到所有的分镜中。": "Select a text file, import the prompts within, split by lines, and apply them sequentially to all storyboards.",
"同步当前分镜的生图提示词到图转视频的提示词中。": "Sync the current storyboard's image generation prompts to the image-to-video prompts.",
"【LaiTool】分镜大师-高图文/视频一致版SD/ComfyUI上下文-人物固定-消耗高)": "【LaiTool】Storyboard Master - High Image-Text/Video Consistency (SD/ComfyUI) (Context-Character Fixed-High Consumption)",
"【LaiTool】分镜大师-高图文/视频一致版MJ上下文-人物固定-消耗高)": "【LaiTool】Storyboard Master - High Image-Text/Video Consistency (MJ) (Context-Character Fixed-High Consumption)"
}

View File

@ -335,9 +335,7 @@ export default {
"错误信息:{error},错误节点:{node}": "错误信息:{error},错误节点:{node}",
"未知错误未获取到请求ID请检查是否正确设置": "未知错误未获取到请求ID请检查是否正确设置",
"ComfyUI 生成图片成功!": "ComfyUI 生成图片成功!",
"ComfyUI生图失败未获取到请求ID请检查是否正确设置": "ComfyUI生图失败未获取到请求ID请检查是否正确设置",
"未获取到ComfyUI的请求地址请检查是否正确设置": "未获取到ComfyUI的请求地址请检查是否正确设置",
"ComfyUI 生图失败,详细失败信息看启动器控制台": 'ComfyUI 生图失败,详细失败信息看启动器控制台',
"FLUX FORGE 生成图片成功!": "FLUX FORGE 生成图片成功!",
"FLUX FORGE 生成图片失败,{error}": "FLUX FORGE 生成图片失败,{error}",
"未知的合并类型": "未知的合并类型",
@ -724,7 +722,7 @@ export default {
'反向提示词': "反向提示词",
'工作流文件检查成功通过': "工作流文件检查成功通过",
"ComfyUI 基础设置": "ComfyUI 基础设置",
'使用工作流': "使用工作流",
'生图工作流': "生图工作流",
'获取ComfyUI通用设置失败': "获取ComfyUI通用设置失败",
'获取ComfyUI工作流设置失败': "获取ComfyUI工作流设置失败",
'添加工作流': "添加工作流",
@ -808,8 +806,6 @@ export default {
"请选择是否开始修脸/修手": "请选择是否开始修脸/修手",
"ADetailer 模型设置": "ADetailer 模型设置",
"请完善所有的关键帧设置!!": '请完善所有的关键帧设置!!',
"LAI API - 香港": "LAI API - 香港",
'LAI API - 美国': 'LAI API - 美国',
'LaiTool生图包': 'LaiTool生图包',
"没有找到对应的API的配置请先检查配置": "没有找到对应的API的配置请先检查配置",
"API模式": "API模式",
@ -1981,4 +1977,85 @@ export default {
"按钮,导入图片": "导入图片",
//#endregion
"【LaiTool】分镜大师-高图文一致版SD/ComfyUI上下文-人物固定)": "【LaiTool】分镜大师-高图文一致版SD/ComfyUI上下文-人物固定)",
"【LaiTool】分镜大师-高图文一致版MJ上下文-人物固定)": "【LaiTool】分镜大师-高图文一致版MJ上下文-人物固定)",
"智络AI - 香港线路": "智络AI - 香港线路",
"智络AI - 美国线路": "智络AI - 美国线路",
"图像生成": "图像生成",
"图像转视频": "图像转视频",
"ComfyUI首帧参考图像不能为空请检查分镜的ComfyUI参数配置": "ComfyUI首帧参考图像不能为空请检查分镜的ComfyUI参数配置",
"ComfyUI首帧参考图像不存在请检查分镜的ComfyUI参数配置": "ComfyUI首帧参考图像不存在请检查分镜的ComfyUI参数配置",
"ComfyUI尾帧参考图像文件不存在请检查分镜的ComfyUI参数配置": "ComfyUI尾帧参考图像文件不存在请检查分镜的ComfyUI参数配置",
"ComfyUI视频生成任务完成": "ComfyUI视频生成任务完成",
"ComfyUI视频生成任务失败失败信息{error}": "ComfyUI视频生成任务失败失败信息{error}",
"ComfyUI图片上传失败未获取到图片名称请检查ComfyUI设置是否正确": "ComfyUI图片上传失败未获取到图片名称请检查ComfyUI设置是否正确",
"当前分镜数据的ComfyUI图转视频参数为空或参数校验失败请检查": "当前分镜数据的ComfyUI图转视频参数为空或参数校验失败请检查",
"未设置选中的工作流,请检查是否正确设置!!": "未设置选中的工作流,请检查是否正确设置!!",
"ComfyUI视频任务失败失败信息{error}": "ComfyUI视频任务失败失败信息{error}",
"ComfyUI视频任务正在执行中...": "ComfyUI视频任务正在执行中...",
"ComfyUI视频任务已完成": "ComfyUI视频任务已完成",
"ComfyUI视频文件信息不完整无法获取视频地址请检查": "ComfyUI视频文件信息不完整无法获取视频地址请检查",
"未获取到请求ID请检查是否正确设置": "未获取到请求ID请检查是否正确设置",
"未找到返回的文件信息": "未找到返回的文件信息",
"详细失败信息看启动器控制台": "详细失败信息看启动器控制台",
"获取工作流列表成功!": "获取工作流列表成功!",
"获取工作流列表失败,{error}": "获取工作流列表失败,{error}",
"获取工作流成功!": "获取工作流成功!",
"获取工作流失败,{error}": "获取工作流失败,{error}",
"添加工作流成功!": "添加工作流成功!",
"添加工作流失败,{error}": "添加工作流失败,{error}",
"修改工作流成功!": "修改工作流成功!",
"修改工作流失败,{error}": "修改工作流失败,{error}",
"删除工作流成功!": "删除工作流成功!",
"删除工作流失败,{error}": "删除工作流失败,{error}",
"批量删除工作流成功!": "批量删除工作流成功!",
"批量删除工作流失败,{error}": "批量删除工作流失败,{error}",
"工作流": "工作流",
"<strong>必选</strong><br/><br/>在 <strong>设置 -> Comfyui 设置</strong> 中设置的类型为 <strong>图转视频</strong> 的工作流": "<strong>必选</strong><br/><br/>在 <strong>设置 -> Comfyui 设置</strong> 中设置的类型为 <strong>图转视频</strong> 的工作流",
"首帧参考图像": "首帧参考图像",
"图片地址": "图片地址",
"<strong>必选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 作为视频生成的起始帧参考<br/>• 本地文件路径": "<strong>必选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 作为视频生成的起始帧参考<br/>• 本地文件路径",
"尾帧参考图像": "尾帧参考图像",
"<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 作为视频生成的起始帧参考<br/>• 本地文件路径": "<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 作为视频生成的起始帧参考<br/>• 本地文件路径",
"描述期望生成的视频内容": "描述期望生成的视频内容",
"描述不希望出现在视频中的内容": "描述不希望出现在视频中的内容",
"例如: 1024": "例如: 1024",
"必填<br/><br/>生成视频的像素分辨率<br/><br/>常用值:<strong>512, 768, 1024, 1920</strong>": "必填<br/><br/>生成视频的像素分辨率<br/><br/>常用值:<strong>512, 768, 1024, 1920</strong>",
"视频时长(秒)": "视频时长(秒)",
"必填<br/><br/>生成视频的持续时间(秒)<br/><br/>初始值会根据分镜时长生成": "必填<br/><br/>生成视频的持续时间(秒)<br/><br/>初始值会根据分镜时长生成",
"是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置工作流文件、分辨率、时长、帧率等基础设置\n\n批量应用后其余分镜的上述基础设置会被替换为当前分镜的数据是否继续": "是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置工作流文件、分辨率、时长、帧率等基础设置\n\n批量应用后其余分镜的上述基础设置会被替换为当前分镜的数据是否继续",
"删除当前搜索出来的所有的预设,若没有搜索条件,会删除全部预设数据!": "删除当前搜索出来的所有的预设,若没有搜索条件,会删除全部预设数据!",
"确定要删除当前搜索出来的所有预设吗?\n若没有任何搜索条件则会删除全部预设数据\n\n此操作不可撤销请谨慎操作": "确定要删除当前搜索出来的所有预设吗?\n若没有任何搜索条件则会删除全部预设数据\n\n此操作不可撤销请谨慎操作",
"正在批量删除预设数据...": "正在批量删除预设数据...",
"批量删除预设成功!": "批量删除预设成功!",
"批量删除预设数据失败,{error}": "批量删除预设数据失败,{error}",
"Midjourney 设置": "Midjourney 设置",
"工作流类型": "工作流类型",
"工作流文件缺少正向提示词、反向提示词或加载首帧图像模块,请检查工作流文件,把对应的文本编码模块的标题改为正向提示词和反向提示词,把加载图像模块的标题改为加载首帧图像!!": "工作流文件缺少正向提示词、反向提示词或加载首帧图像模块,请检查工作流文件,把对应的文本编码模块的标题改为正向提示词和反向提示词,把加载图像模块的标题改为加载首帧图像!!",
"未知的工作流类型,请检查工作流类型": "未知的工作流类型,请检查工作流类型",
"视频工作流": "视频工作流",
"⚠️ ComfyUI 工作流配置请严格参考文档,否则无法正常生成!": "⚠️ ComfyUI 工作流配置请严格参考文档,否则无法正常生成!",
"查看文档": "查看文档",
"工作流设置": "工作流设置",
"批量删除({count}": "批量删除({count}",
"加载工作流数据失败,{error}": "加载工作流数据失败,{error}",
"请先选择要删除的工作流": "请先选择要删除的工作流",
"确定要删除选中的 {count} 个工作流吗?此操作不可撤销。是否继续?": "确定要删除选中的 {count} 个工作流吗?此操作不可撤销。是否继续?",
"批量删除工作流失败:{error}": "批量删除工作流失败:{error}",
"批量删除工作流成功": "批量删除工作流成功",
"获取ComfyUI工作流失败{error}": "获取ComfyUI工作流失败{error}",
"同步主图文件": "同步主图文件",
"同步子图文件": "同步子图文件",
"正在导入提示词...": "正在导入提示词...",
"该操作会同步生图提示词到图转视频的提示词,若不存在生图提示词则跳过当前分镜,同步操作不可逆!\n\n是否继续": "该操作会同步生图提示词到图转视频的提示词,若不存在生图提示词则跳过当前分镜,同步操作不可逆!\n\n是否继续",
"正在同步提示词...": "正在同步提示词...",
"同步第 {line} 行提示词失败,{error}": "同步第 {line} 行提示词失败,{error}",
"同步提示词成功!": "同步提示词成功!",
"同步提示词失败,{error}": "同步提示词失败,{error}",
"同步生图提示词": "同步生图提示词",
"选择一个文本文件,导入其中的提示词,按行分割,依次应用到所有的分镜中。": "选择一个文本文件,导入其中的提示词,按行分割,依次应用到所有的分镜中。",
"同步当前分镜的生图提示词到图转视频的提示词中。": "同步当前分镜的生图提示词到图转视频的提示词中。",
"【LaiTool】分镜大师-高图文/视频一致版SD/ComfyUI上下文-人物固定-消耗高)": "【LaiTool】分镜大师-高图文/视频一致版SD/ComfyUI上下文-人物固定-消耗高)",
"【LaiTool】分镜大师-高图文/视频一致版MJ上下文-人物固定-消耗高)": "【LaiTool】分镜大师-高图文/视频一致版MJ上下文-人物固定-消耗高)"
}

View File

@ -159,6 +159,7 @@ export class BookBasicHandle {
&& task.type != BookBackTaskType.HAILUO_TEXT_TO_VIDEO
&& task.type != BookBackTaskType.HAILUO_IMAGE_TO_VIDEO
&& task.type != BookBackTaskType.HAILUO_FIRST_LAST_FRAME
&& task.type != BookBackTaskType.COMFYUI_VIDEO
) {
// 转存一下视频文件
// 获取当前url的文件名

View File

@ -374,15 +374,15 @@ export class BookPromptHandle extends BookBasicHandle {
let newData: BookTask.BookTaskCharacterAndSceneObject[] = []
for (let i = 0; i < returnData.length; i++) {
const element = returnData[i]
let splitData = element.split('.')
if (splitData.length < 3) {
let splitData = element.split('')
if (splitData.length < 2) {
continue
}
let tempData = {
no: Number(splitData[0]),
no: i + 1,
id: crypto.randomUUID(),
name: splitData[1],
prompt: splitData[2]
name: splitData[0],
prompt: splitData[1]
} as BookTask.BookTaskCharacterAndSceneObject
newData.push(tempData)
}

View File

@ -198,6 +198,14 @@ export class BookVideoServiceHandle extends BookBasicHandle {
if (gptUrl == null || isEmpty(gptUrl)) {
throw new Error(t('未找到有效的GPT API地址'))
}
let duration = 5;
if (bookTaskDetail.endTime != null && bookTaskDetail.startTime != null) {
let d = (bookTaskDetail.endTime - bookTaskDetail.startTime) / 1000000;
duration = Math.ceil(d); // 向上取整
}
// 开始设置默认设置
let outImage =
@ -281,6 +289,17 @@ export class BookVideoServiceHandle extends BookBasicHandle {
resolution: HailuoResolution.P768,
}
let comfyuiOptions: BookTaskDetail.ComfyUIOptions = {
workflow_file: "",
first_frame_image: outImage,
last_frame_image: "",
prompt: "",
negative_prompt: "",
resolution: 720,
duration: duration,
fps: 30
}
let videoMessage: BookTaskDetail.VideoMessage = {
id: bookTaskDetail.id,
msg: '',
@ -293,6 +312,10 @@ export class BookVideoServiceHandle extends BookBasicHandle {
lumaOptions: JSON.stringify(lumaOptions),
klingOptions: JSON.stringify(klingOptions),
mjVideoOptions: JSON.stringify(mjVideoOptions),
hailuoTextToVideoOptions: JSON.stringify(hailuoTextToVideoOptions),
hailuoFirstFrameOnlyOptions: JSON.stringify(hailuoFirstFrameOnlyOptions),
hailuoFirstLastFrameOptions: JSON.stringify(hailuoFirstLastFrameOptions),
comfyUIOptions: JSON.stringify(comfyuiOptions),
status: VideoStatus.WAIT,
model: VideoModel.IMAGE_TO_VIDEO
}
@ -366,6 +389,9 @@ export class BookVideoServiceHandle extends BookBasicHandle {
case BookBackTaskType.HAILUO_FIRST_LAST_FRAME:
res = await videoHandle.HailuoFirstLastFrameToVideo(task)
break
case BookBackTaskType.COMFYUI_VIDEO:
res = await videoHandle.ComfyUIImageToVideo(task)
break
default:
throw new Error(t('未知的视频生成方式,请检查'))
}

View File

@ -11,9 +11,9 @@ import {
import fs from 'fs'
import { ValidateJson } from '@/define/Tools/validate'
import axios from 'axios'
import { isEmpty } from 'lodash'
import { cloneDeep, isEmpty } from 'lodash'
import { BookBackTaskStatus, BookTaskStatus, OperateBookType } from '@/define/enum/bookEnum'
import { SendReturnMessage } from '@/public/generalTools'
import { SendReturnMessage, successMessage } from '@/public/generalTools'
import { Book } from '@/define/model/book/book'
import { MJAction } from '@/define/enum/mjEnum'
import { ImageGenerateMode } from '@/define/data/mjData'
@ -21,16 +21,26 @@ import path from 'path'
import { getProjectPath } from '../option/optionCommonService'
import { SDServiceHandle } from './sdServiceHandle'
import { t } from '@/i18n'
import { WorkflowModel } from '@/define/model/workflow'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import { VideoStatus } from '@/define/enum/video'
import { ResponseMessageType } from '@/define/enum/softwareEnum'
import { ComfyUIWorkflowType } from '@/define/enum/comfyuiEnum'
export class ComfyUIServiceHandle extends SDServiceHandle {
constructor() {
super()
}
//#region 生图主流程
/**
* ComfyUI生图流程
* @param task
*/
ComfyUIImageGenerate = async (task: TaskModal.Task) => {
try {
await this.InitSDBasic()
let comfyUISettingCollection = await this.GetComfyUISetting()
let comfyuiSimpleSetting = await this.GetComfyUISetting()
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
task.bookTaskDetailId as string, true
)
@ -54,17 +64,19 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
bookTaskDetail.prompt = mergeRes.data[0].prompt
let prompt = bookTaskDetail.prompt
let negativePrompt = comfyUISettingCollection.comfyuiSimpleSetting.negativePrompt
let negativePrompt = comfyuiSimpleSetting.negativePrompt
let imageWorkflow = await this.GetComfyUIWorkflow(comfyuiSimpleSetting.selectedWorkflow as string);
// 开始组合请求体
let body = await this.GetComfyUIAPIBody(
let body = await this.GetComfyUIImageAPIBody(
prompt ?? '',
negativePrompt ?? '',
comfyUISettingCollection.comfyuiSelectedWorkflow.workflowPath
imageWorkflow.workflowFilePath
)
// 开始发送请求
let resData = await this.SubmitComfyUIImagine(body, comfyUISettingCollection)
let resData = await this.SubmitComfyUITask(body, comfyuiSimpleSetting)
// 修改任务状态
await this.bookTaskDetailService.ModifyBookTaskDetailById(task.bookTaskDetailId as string, {
@ -88,13 +100,13 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
task.messageName as string
)
await this.FetchImageTask(
await this.FetchComfyUIImageTask(
task,
resData.prompt_id,
book,
bookTask,
bookTaskDetail,
comfyUISettingCollection
comfyuiSimpleSetting
)
} catch (error: any) {
let errorMsg = t("ComfyUI生图失败{error}", {
@ -105,7 +117,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
status: BookBackTaskStatus.FAIL,
errorMessage: errorMsg
})
await this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
mjApiUrl: '',
@ -136,64 +148,298 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
throw error
}
}
//#endregion
//#region 获取ComfyUI的设置
/**
* ComfyUI的设置
* @returns
*/
private async GetComfyUISetting(): Promise<SettingModal.ComfyUISettingCollection> {
let result = {} as SettingModal.ComfyUISettingCollection
let optionRealmService = await OptionRealmService.getInstance()
let comfyuiSimpleSettingOption = optionRealmService.GetOptionByKey(
OptionKeyName.SD.ComfyUISimpleSetting
)
result['comfyuiSimpleSetting'] = optionSerialization<SettingModal.ComfyUISimpleSettingModel>(
comfyuiSimpleSettingOption,
t("设置 -> ComfyUI 设置")
//#region 生成视频主流程
ComfyUIVideoGenerate = async (task: TaskModal.Task) => {
try {
await this.InitSDBasic()
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
task.bookTaskDetailId as string, true
)
let comfyuiWorkFlowSettingOption = optionRealmService.GetOptionByKey(
OptionKeyName.SD.ComfyUIWorkFlowSetting
)
let { comfyuiOptions, videoMessage } = await this.GetComfyuiOptions(bookTaskDetail)
let comfyuiWorkFlowList = optionSerialization<SettingModal.ComfyUIWorkFlowSettingModel[]>(
comfyuiWorkFlowSettingOption,
t("设置 -> ComfyUI 设置")
)
result['comfyuiWorkFlowSetting'] = comfyuiWorkFlowList
let comfyuiSimpleSetting = await this.GetComfyUISetting()
if (comfyuiWorkFlowList.length <= 0) {
throw new Error(t('ComfyUI的工作流设置为空请检查是否正确设置'))
let workflow_file = comfyuiOptions.workflow_file ?? comfyuiSimpleSetting.imageToVideoSelectWorkflow ?? "";
let prompt = videoMessage.prompt || comfyuiOptions.prompt || '';
let negativePrompt = comfyuiOptions.negative_prompt || '';
let firstFrameImage = videoMessage.imageUrl ?? comfyuiOptions.first_frame_image ?? "";
let lastFrameImage = comfyuiOptions.last_frame_image ?? "";
let resolution = comfyuiOptions.resolution ?? 768;
let duration = comfyuiOptions.duration ?? 5;
let fps = comfyuiOptions.fps ?? 24;
let workflow = await this.GetComfyUIWorkflow(workflow_file);
// 检查图片是否存在并且上传
let firstFrameImageName: string;
if (isEmpty(firstFrameImage)) {
throw new Error(t('ComfyUI首帧参考图像不能为空请检查分镜的ComfyUI参数配置'))
} else {
if (!await CheckFileOrDirExist(firstFrameImage)) {
throw new Error(t('ComfyUI首帧参考图像不存在请检查分镜的ComfyUI参数配置'))
}
// 获取选中的工作流
let selectedWorkflow = comfyuiWorkFlowList.find(
(item) => item.id == result.comfyuiSimpleSetting.selectedWorkflow
firstFrameImageName = await this.ComfyUIUploadImage(firstFrameImage, comfyuiSimpleSetting);
}
let lastFrameImageName: string = "";
if (!isEmpty(lastFrameImage)) {
if (!await CheckFileOrDirExist(lastFrameImage)) {
throw new Error(t('ComfyUI尾帧参考图像文件不存在请检查分镜的ComfyUI参数配置'))
}
lastFrameImageName = await this.ComfyUIUploadImage(lastFrameImage, comfyuiSimpleSetting);
}
// 开始处理body
let bodyString = await this.GetComfyUIVideoAPIBody({
prompt: prompt,
negativePrompt: negativePrompt,
workflowFilePath: workflow.workflowFilePath,
firstFrameImageName: firstFrameImageName,
lastFrameImageName: lastFrameImageName ?? "",
resolution: resolution,
duration: duration,
fps: fps
})
// 开始发送请求
let resData = await this.SubmitComfyUITask(bodyString, comfyuiSimpleSetting)
let taskId = resData.prompt_id;
// 修改Task, 将数据写入
this.taskListService.UpdateBackTaskData(task.id as string, {
taskId: taskId as string,
taskMessage: JSON.stringify(resData)
})
// 修改videoMessage
videoMessage.taskId = taskId
videoMessage.status = VideoStatus.SUBMITTED
videoMessage.messageData = JSON.stringify(resData)
videoMessage.msg = ''
delete videoMessage.imageUrl // 不要修改原本的图片地址
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
videoMessage
)
if (selectedWorkflow == null) {
throw new Error(t('未找到选中的工作流,请检查是否正确设置!!'))
}
// 判断工作流对应的文件是不是存在
if (!(await CheckFileOrDirExist(selectedWorkflow.workflowPath))) {
throw new Error(t('本地未找到选中的工作流文件地址,请检查是否正确设置!!'))
}
result['comfyuiSelectedWorkflow'] = selectedWorkflow
// 添加任务成功 返回前端任务事件
SendReturnMessage(
{
code: 1,
id: task.bookTaskDetailId as string,
message: t('已成功提交{type}图转视频任务任务ID{taskId}', { type: t("ComfyUI"), taskId: taskId }),
type: ResponseMessageType.COMFYUI_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
return result
await this.FetchComfyUIVideoTask(task, taskId, bookTaskDetail, comfyuiSimpleSetting)
return successMessage(
t('ComfyUI视频生成任务完成'),
)
} catch (error) {
throw new Error(t('ComfyUI视频生成任务失败失败信息{error}', { error: (error as Error).message }));
}
}
//#endregion
//#region 组合ComfyUI的请求体
//#region comfyUI上传图片
private async ComfyUIUploadImage(imagePath: string, comfyuiSimpleSetting: SettingModal.ComfyUISimpleSettingModel): Promise<string> {
var FormData = require('form-data');
var data = new FormData();
data.append('image', fs.createReadStream(imagePath));
data.append('type', 'input');
let url = comfyuiSimpleSetting.requestUrl?.replace(
'localhost',
'127.0.0.1'
)
if (url.endsWith('/')) {
url = url + 'api/upload/image'
} else {
url = url + '/api/upload/image'
}
var config = {
method: 'post',
url: url,
headers: {
'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
...data.getHeaders()
},
data: data
};
let res = await axios(config);
let resData = res.data;
if (isEmpty(resData.name)) {
throw new Error(t('ComfyUI图片上传失败未获取到图片名称请检查ComfyUI设置是否正确'))
}
return resData.name;
}
//#endregion
//#region 获取ComfyuiOptions
private async GetComfyuiOptions(bookTaskDetail: Book.SelectBookTaskDetail)
: Promise<{ comfyuiOptions: BookTaskDetail.ComfyUIOptions, videoMessage: BookTaskDetail.VideoMessage }> {
let videoMessage = bookTaskDetail.videoMessage;
if (videoMessage == null || videoMessage == undefined) {
throw new Error(t('小说批次任务的分镜数据的转视频配置为空,请检查'))
}
let comfyuiOptions: BookTaskDetail.ComfyUIOptions;
let comfyuiOptionsString = videoMessage.comfyUIOptions as string;
if (!ValidateJson(comfyuiOptionsString)) {
throw new Error(t('当前分镜数据的ComfyUI图转视频参数为空或参数校验失败请检查'))
}
comfyuiOptions = JSON.parse(comfyuiOptionsString) as BookTaskDetail.ComfyUIOptions;
return { comfyuiOptions, videoMessage };
}
//#endregion
//#region 获取设置
/**
* ComfyUI的请求体
* ComfyUI的设置
* @returns
*/
private async GetComfyUISetting(): Promise<SettingModal.ComfyUISimpleSettingModel> {
let optionRealmService = await OptionRealmService.getInstance()
// 获取ComfyUI的基础设置
let comfyuiSimpleSettingOption = optionRealmService.GetOptionByKey(
OptionKeyName.SD.ComfyUISimpleSetting
)
let res = optionSerialization<SettingModal.ComfyUISimpleSettingModel>(
comfyuiSimpleSettingOption,
t("设置 -> ComfyUI 设置")
)
return res;
}
private async GetComfyUIWorkflow(workflowId: string): Promise<WorkflowModel.Workflow> {
if (isEmpty(workflowId)) {
throw new Error(t('未设置选中的工作流,请检查是否正确设置!!'))
}
// 查找工作流
let workflowRes: WorkflowModel.Workflow = this.workflowRealmService.GetWorkFlowById(workflowId as string, true);
// 判断工作流对应的文件是不是存在
if (!(await CheckFileOrDirExist(workflowRes.workflowFilePath))) {
throw new Error(t('本地未找到选中的工作流文件地址,请检查是否正确设置!!'))
}
return workflowRes
}
//#endregion
//#region 生成视频的请求体
private async GetComfyUIVideoAPIBody(params: SettingModal.ComfyUIVideoAPIBodyGenerateQuery): Promise<string> {
let jsonContentString = await fs.promises.readFile(params.workflowFilePath, 'utf-8')
if (!ValidateJson(jsonContentString)) {
throw new Error(t('工作流文件内容不是有效的JSON格式请检查是否正确设置'))
}
let jsonContent = JSON.parse(jsonContentString)
// 判断是否是对象
if (jsonContent !== null && typeof jsonContent === 'object' && !Array.isArray(jsonContent)) {
// 遍历对象属性
for (const key in jsonContent) {
let element = jsonContent[key]
// 处理正向提示词和反向提示词
if (element && element.class_type === 'CLIPTextEncode') {
if (element._meta?.title === '正向提示词' || element._meta?.title === 'Positive Prompt') {
jsonContent[key].inputs.text = params.prompt
}
if (element._meta?.title === '反向提示词' || element._meta?.title === 'Negative Prompt') {
jsonContent[key].inputs.text = params.negativePrompt
}
}
// 处理首尾帧图像传入
if (element && element.class_type === 'LoadImage') {
if (element._meta?.title === '加载首帧图像' || element._meta?.title === 'Load First Frame Image') {
jsonContent[key].inputs.image = params.firstFrameImageName;
}
if (element._meta?.title === '加载尾帧图像' || element._meta?.title === 'Load Last Frame Image') {
jsonContent[key].inputs.image = params.lastFrameImageName;
}
}
// 处理视频时长,分辨率
if (element && element.class_type === 'Int') {
if (element._meta?.title === '视频时长' || element._meta?.title === 'Video Duration') {
jsonContent[key].inputs.Number = params.duration * 16 + 1;
}
if (element._meta?.title === '分辨率' || element._meta?.title === 'Resolution') {
jsonContent[key].inputs.Number = params.resolution.toString();
}
}
// 处理随机seed
if (element && element.class_type === 'KSampler') {
let seed = this.GenerateRandomSeed();
jsonContent[key].inputs.seed = seed;
} else if (element && element.class_type === 'KSamplerAdvanced') {
let seed = this.GenerateRandomSeed();
jsonContent[key].inputs.noise_seed = seed
}
}
} else {
throw new Error(t('工作流文件内容不是有效的JSON对象格式请检查是否正确设置'))
}
let res = JSON.stringify({
prompt: jsonContent
})
return res
}
//#endregion
//#region 生成随机seed
private GenerateRandomSeed(): string {
const crypto = require('crypto')
const buffer = crypto.randomBytes(8)
let seed = BigInt('0x' + buffer.toString('hex'))
return seed.toString();
}
//#endregion
//#region 组合图片的请求体
/**
* ComfyUI生成图片的的请求体
* @param prompt
* @param negativePrompt
* @param workflowPath
*/
private async GetComfyUIAPIBody(
private async GetComfyUIImageAPIBody(
prompt: string,
negativePrompt: string,
workflowPath: string
@ -219,17 +465,13 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
}
}
if (element && element.class_type === 'KSampler') {
const crypto = require('crypto')
const buffer = crypto.randomBytes(8)
let seed = BigInt('0x' + buffer.toString('hex'))
let seed = this.GenerateRandomSeed();
jsonContent[key].inputs.seed = seed.toString()
jsonContent[key].inputs.seed = seed;
} else if (element && element.class_type === 'KSamplerAdvanced') {
const crypto = require('crypto')
const buffer = crypto.randomBytes(8)
let seed = BigInt('0x' + buffer.toString('hex'))
let seed = this.GenerateRandomSeed();
jsonContent[key].inputs.noise_seed = seed.toString()
jsonContent[key].inputs.noise_seed = seed
}
}
} else {
@ -243,13 +485,17 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
//#endregion
//#region 提交ComfyUI生成图片任务
private async SubmitComfyUIImagine(
//#region 提交执行任务
/**
* ComfyUI服务器
* @param body
* @param comfyUISettingCollection ComfyUI
*/
private async SubmitComfyUITask(
body: string,
comfyUISettingCollection: SettingModal.ComfyUISettingCollection
comfyuiSimpleSetting: SettingModal.ComfyUISimpleSettingModel
): Promise<any> {
let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace(
let url = comfyuiSimpleSetting.requestUrl?.replace(
'localhost',
'127.0.0.1'
)
@ -295,19 +541,190 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
//#endregion
//#region 获取出图任务
//#region 循环获取视频任务
async FetchImageTask(
async FetchComfyUIVideoTask(
task: TaskModal.Task,
promptId: string,
bookTaskDetail: Book.SelectBookTaskDetail,
comfyuiSimpleSetting: SettingModal.ComfyUISimpleSettingModel) {
while (true) {
try {
let resData = await this.GetComfyUITaskOnce(promptId, comfyuiSimpleSetting, ComfyUIWorkflowType.IMAGE_TO_VIDEO);
// 判断他的状态是不是成功
if (resData.status == 'error') {
// 修改小说分镜的 videoMessage
let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {}
videoMessage.status = VideoStatus.FAIL
videoMessage.msg = resData.message
videoMessage.taskId = promptId
videoMessage.messageData = JSON.stringify(resData)
delete videoMessage.imageUrl
// 修改 videoMessage数据
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
bookTaskDetail.id as string,
videoMessage
)
// 修改TASK
this.taskListService.UpdateBackTaskData(task.id as string, {
taskId: promptId,
taskMessage: JSON.stringify(resData)
})
// 返回前端数据
SendReturnMessage(
{
code: 0,
id: bookTaskDetail.id as string,
message: t("ComfyUI视频任务失败失败信息{error}", {
error: resData.message
}),
type: ResponseMessageType.COMFYUI_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
throw new Error(resData.message)
} else if (resData.status == 'in_progress') {
// 任务执行中或者是提交成功
let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {}
videoMessage.status = VideoStatus.PROCESSING
videoMessage.taskId = promptId;
videoMessage.messageData = JSON.stringify(resData)
delete videoMessage.imageUrl
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
videoMessage
)
SendReturnMessage(
{
code: 1,
id: bookTaskDetail.id as string,
message: t('ComfyUI视频任务正在执行中...'),
type: ResponseMessageType.COMFYUI_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
await new Promise((resolve) => setTimeout(resolve, 20000))
}
else {
// 任务成功 修改 videoMessage
let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {}
videoMessage.status = VideoStatus.SUCCESS
videoMessage.taskId = promptId
videoMessage.messageData = JSON.stringify(resData)
delete videoMessage.imageUrl
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
videoMessage
)
// 修改小说分镜状态
this.bookTaskDetailService.ModifyBookTaskDetailById(task.bookTaskDetailId as string, {
status: BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS
})
// 修改任务状态
this.taskListService.UpdateBackTaskData(task.id as string, {
status: BookBackTaskStatus.DONE,
taskId: promptId,
taskMessage: JSON.stringify(resData)
})
let videoUrls: string[] = [];
videoMessage.videoUrls = [];
for (let i = 0; resData.fileInfo && i < resData.fileInfo.length; i++) {
const element = resData.fileInfo[i];
if (!isEmpty(element.filename)) {
let videoUrl = this.GetComfyUIVideoUrl(element, comfyuiSimpleSetting);
videoUrls.push(videoUrl);
videoMessage.videoUrls.push(videoUrl);
}
}
await this.DownloadVideoUrls(videoMessage.videoUrls || [], task, bookTaskDetail, promptId);
SendReturnMessage(
{
code: 1,
id: bookTaskDetail.id as string,
message: t('ComfyUI视频任务已完成'),
type: ResponseMessageType.COMFYUI_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
break;
}
} catch (error) {
throw error;
}
}
}
//#endregion
//#region 拼接视频地址
private GetComfyUIVideoUrl(fileInfo: any, comfyuiSimpleSetting: SettingModal.ComfyUISimpleSettingModel): string {
let url = comfyuiSimpleSetting.requestUrl?.replace(
'localhost',
'127.0.0.1'
)
if (url.endsWith('/')) {
url = url + 'api/view'
} else {
url = url + '/api/view'
}
const queryParams = new URLSearchParams();
for (const [key, value] of Object.entries(fileInfo)) {
if (value !== null && value !== undefined) {
queryParams.append(key, String(value));
}
}
const queryString = queryParams.toString();
if (isEmpty(queryString)) {
throw new Error(t('ComfyUI视频文件信息不完整无法获取视频地址请检查'))
}
let videoUrl = `${url}?${queryString}`;
return videoUrl;
}
//#endregion
//#region 循环获取出图任务
/**
*
* @param task
* @param promptId ComfyUI返回的任务ID
* @param book
* @param bookTask
* @param bookTaskDetail
* @param comfyUISettingCollection ComfyUI
*/
async FetchComfyUIImageTask(
task: TaskModal.Task,
promptId: string,
book: Book.SelectBook,
bookTask: Book.SelectBookTask,
bookTaskDetail: Book.SelectBookTaskDetail,
comfyUISettingCollection: SettingModal.ComfyUISettingCollection
comfyuiSimpleSetting: SettingModal.ComfyUISimpleSettingModel
) {
while (true) {
try {
let resData = await this.GetComfyUIImageTask(promptId, comfyUISettingCollection)
let resData = await this.GetComfyUITaskOnce(promptId, comfyuiSimpleSetting, ComfyUIWorkflowType.IMAGE)
// 判断他的状态是不是成功
if (resData.status == 'error') {
@ -324,7 +741,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl,
mjApiUrl: comfyuiSimpleSetting.requestUrl,
progress: 100,
category: ImageGenerateMode.ComfyUI,
imageClick: '',
@ -359,7 +776,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl,
mjApiUrl: comfyuiSimpleSetting.requestUrl,
progress: 0,
category: ImageGenerateMode.ComfyUI,
imageClick: '',
@ -386,8 +803,8 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
)
} else {
let res = await this.DownloadFileUrl(
resData.imageNames,
comfyUISettingCollection,
resData.fileInfo.filter((item: any) => !isEmpty(item.filename)).map((item: any) => item.filename),
comfyuiSimpleSetting,
book,
bookTask,
bookTaskDetail
@ -408,7 +825,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
mjApiUrl: comfyUISettingCollection.comfyuiSimpleSetting.requestUrl,
mjApiUrl: comfyuiSimpleSetting.requestUrl,
progress: 100,
category: ImageGenerateMode.ComfyUI,
imageClick: '',
@ -446,25 +863,26 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
//#endregion
//#region 获取comfyui出图任务
//#region 单次获取任务
/**
* ComfyUI出图任务
* ComfyUI任务
* @param promptId
* @param comfyUISettingCollection
*/
private async GetComfyUIImageTask(
private async GetComfyUITaskOnce(
promptId: string,
comfyUISettingCollection: SettingModal.ComfyUISettingCollection
comfyuiSimpleSetting: SettingModal.ComfyUISimpleSettingModel,
type: ComfyUIWorkflowType
): Promise<any> {
if (isEmpty(promptId)) {
throw new Error(t("ComfyUI生图失败未获取到请求ID请检查是否正确设置"))
throw new Error(t("未获取到请求ID请检查是否正确设置"))
}
if (isEmpty(comfyUISettingCollection.comfyuiSimpleSetting.requestUrl)) {
if (isEmpty(comfyuiSimpleSetting.requestUrl)) {
throw new Error(t('未获取到ComfyUI的请求地址请检查是否正确设置'))
}
let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace(
let url = comfyuiSimpleSetting.requestUrl?.replace(
'localhost',
'127.0.0.1'
)
@ -497,26 +915,50 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
let completed = data.status?.completed
let outputs = data.outputs
if (completed && outputs) {
let imageNames: string[] = []
let fileInfo: string[] = []
for (const key in outputs) {
let outputNode = outputs[key]
if (type == ComfyUIWorkflowType.IMAGE) {
if (outputNode && outputNode?.images && outputNode?.images.length > 0) {
for (let i = 0; i < outputNode?.images.length; i++) {
const element = outputNode?.images[i]
imageNames.push(element.filename as string)
if (!isEmpty(element.filename)) {
fileInfo.push(element)
}
}
}
}
if (type == ComfyUIWorkflowType.IMAGE_TO_VIDEO) {
if (outputNode && outputNode?.gifs && outputNode?.gifs.length > 0) {
for (let i = 0; i < outputNode?.gifs.length; i++) {
const element = outputNode?.gifs[i]
if (!isEmpty(element.filename)) {
fileInfo.push(element)
}
}
}
}
}
if (fileInfo.length == 0) {
return {
progress: 0,
status: 'error',
message: t('未找到返回的文件信息')
}
}
return {
progress: 100,
status: 'success',
imageNames: imageNames
fileInfo: fileInfo
}
} else {
return {
progress: 0,
status: 'error',
message: t('ComfyUI 生图失败,详细失败信息看启动器控制台')
message: t('详细失败信息看启动器控制台')
}
}
}
@ -532,7 +974,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
*/
private async DownloadFileUrl(
imageNames: string[],
comfyUISettingCollection: SettingModal.ComfyUISettingCollection,
comfyuiSimpleSetting: SettingModal.ComfyUISimpleSettingModel,
book: Book.SelectBook,
bookTask: Book.SelectBookTask,
bookTaskDetail: Book.SelectBookTaskDetail
@ -540,7 +982,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
outImagePath: string
subImagePath: string[]
}> {
let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace(
let url = comfyuiSimpleSetting.requestUrl?.replace(
'localhost',
'127.0.0.1'
)

View File

@ -2,25 +2,102 @@ import { TaskModal } from '@/define/model/task'
import { SDServiceHandle } from './sdServiceHandle'
import { FluxServiceHandle } from './fluxServiceHandle'
import { ComfyUIServiceHandle } from './comfyUIServiceHandle'
import { WorkflowServiceHandle } from './workflowServiceHandle'
import { WorkflowModel } from '@/define/model/workflow'
export class SDHandle {
sdServiceHandle: SDServiceHandle
fluxServiceHandle: FluxServiceHandle
comfyUIServiceHandle: ComfyUIServiceHandle
workflowServiceHandle: WorkflowServiceHandle
constructor() {
this.sdServiceHandle = new SDServiceHandle()
this.fluxServiceHandle = new FluxServiceHandle()
this.comfyUIServiceHandle = new ComfyUIServiceHandle()
this.workflowServiceHandle = new WorkflowServiceHandle()
}
//#region SD
/** 使用Stable Diffusion生成图像 */
SDImageGenerate = async (task: TaskModal.Task) => await this.sdServiceHandle.SDImageGenerate(task)
//#endregion
//#region FLUX
/** 使用 Flux FORGE 生成图片 */
FluxForgeImageGenerate = async (task: TaskModal.Task) =>
await this.fluxServiceHandle.FluxForgeImageGenerate(task)
//#endregion
//#region Comfy UI
/** 使用 Comfy UI 生成图片 */
ComfyUIImageGenerate = async (task: TaskModal.Task) =>
await this.comfyUIServiceHandle.ComfyUIImageGenerate(task)
/** 使用 Comfy UI 生成视频 */
ComfyUIVideoGenerate = async (task: TaskModal.Task) =>
await this.comfyUIServiceHandle.ComfyUIVideoGenerate(task)
//#endregion
//#region Workflow
/**
*
* @param condition idnametypeworkflowFilePathpagepageSize等
* @returns
*/
GetWorkFlowByCondition = async (condition: WorkflowModel.QueryWorkflowCondition) =>
await this.workflowServiceHandle.GetWorkFlowByCondition(condition)
/**
* ID查询单个工作流
* @param id
* @returns null的成功或错误消息
*/
GetWorkFlowById = async (id: string) => await this.workflowServiceHandle.GetWorkFlowById(id)
/**
* ID查询工作流
* @param ids ID数组
* @returns
*/
GetWorkFlowByIds = async (ids: string[]) => await this.workflowServiceHandle.GetWorkFlowByIds(ids)
/**
*
* @param workflow nametypeworkflowFilePath等必要字段
* @returns
*/
AddWorkFlow = async (workflow: Partial<WorkflowModel.Workflow>) =>
await this.workflowServiceHandle.AddWorkFlow(workflow)
/**
* ID的工作流
* @param id
* @param workflow
* @returns
*/
ModifyWorkflow = async (id: string, workflow: Partial<WorkflowModel.Workflow>) =>
await this.workflowServiceHandle.ModifyWorkflow(id, workflow)
/**
* ID的工作流
* @param id
* @returns
*/
DeleteWorkflow = async (id: string) => await this.workflowServiceHandle.DeleteWorkflow(id)
/**
* ID对应的工作流
* @param ids ID数组
* @returns
*/
DeleteWorkflowByIds = async (ids: string[]) => await this.workflowServiceHandle.DeleteWorkflowByIds(ids)
//#endregion
}

View File

@ -1,25 +1,17 @@
import { OptionRealmService } from '@/define/db/service/optionService'
import { OptionKeyName } from '@/define/enum/option'
import { optionSerialization } from '../option/optionSerialization'
import { SettingModal } from '@/define/model/setting'
import { BookTaskDetailService } from '@/define/db/service/book/bookTaskDetailService'
import { BookTaskService } from '@/define/db/service/book/bookTaskService'
import { BookService } from '@/define/db/service/book/bookService'
import { TaskListService } from '@/define/db/service/book/taskListService'
import { PresetRealmService } from '@/define/db/service/presetService'
import { WorkflowRealmService } from '@/define/db/service/workflowService'
import { BookBasicHandle } from '../book/subBookHandle/bookBasicHandle'
export class SDBasic {
optionRealmService!: OptionRealmService
presetRealmService!: PresetRealmService
export class SDBasic extends BookBasicHandle {
sdImageSetting!: SettingModal.SDSettings
sdADetailerSetting!: SettingModal.SDADetailerModel[]
bookTaskDetailService!: BookTaskDetailService
taskListService!: TaskListService
bookService!: BookService
bookTaskService!: BookTaskService
workflowRealmService!: WorkflowRealmService
adetailerParam: any[] = []
constructor() {}
constructor() {
super()
}
/**
* SDBasic类的基础服务
@ -39,23 +31,9 @@ export class SDBasic {
*/
async InitSDBasic(): Promise<void> {
// 如果 optionRealmService 已经初始化,则直接返回
if (!this.optionRealmService) {
this.optionRealmService = await OptionRealmService.getInstance()
}
if (!this.bookTaskDetailService) {
this.bookTaskDetailService = await BookTaskDetailService.getInstance()
}
if (!this.bookTaskService) {
this.bookTaskService = await BookTaskService.getInstance()
}
if (!this.bookService) {
this.bookService = await BookService.getInstance()
}
if (!this.taskListService) {
this.taskListService = await TaskListService.getInstance()
}
if (!this.presetRealmService) {
this.presetRealmService = await PresetRealmService.getInstance()
await this.InitBookBasicHandle();
if (!this.workflowRealmService) {
this.workflowRealmService = await WorkflowRealmService.getInstance()
}
}

View File

@ -0,0 +1,152 @@
import { WorkflowModel } from "@/define/model/workflow";
import { SDBasic } from "./sdBasic";
import { ErrorItem, SuccessItem } from "@/define/model/generalResponse";
import { errorMessage, successMessage } from "@/public/generalTools";
import { t } from "@/i18n";
export class WorkflowServiceHandle extends SDBasic {
constructor() {
super()
}
/**
*
* @param condition idnametypeworkflowFilePathpagepageSize等
* @returns
*/
GetWorkFlowByCondition = async (condition: WorkflowModel.QueryWorkflowCondition): Promise<SuccessItem | ErrorItem> => {
try {
await this.InitSDBasic()
let res = this.workflowRealmService.GetWorkFlowByCondition(condition)
return successMessage(res, t("获取工作流列表成功!"), 'WorkflowServiceHandle_GetWorkFlowByCondition')
} catch (error) {
return errorMessage(
t("获取工作流列表失败,{error}", {
error: (error as Error).message
}),
'WorkflowServiceHandle_GetWorkFlowByCondition'
)
}
}
/**
* ID查询单个工作流
* @param id
* @returns null的成功或错误消息
*/
GetWorkFlowById = async (id: string): Promise<SuccessItem | ErrorItem> => {
try {
await this.InitSDBasic()
let res = this.workflowRealmService.GetWorkFlowById(id)
return successMessage(res, "获取工作流成功!", 'WorkflowServiceHandle_GetWorkFlowById')
} catch (error) {
return errorMessage(
t('获取工作流失败,{error}', {
error: (error as Error).message
}),
'WorkflowServiceHandle_GetWorkFlowById'
)
}
}
/**
* ID查询工作流
* @param ids ID数组
* @returns
*/
GetWorkFlowByIds = async (ids: string[]): Promise<SuccessItem | ErrorItem> => {
try {
await this.InitSDBasic()
let res = this.workflowRealmService.GetWorkFlowByIds(ids)
return successMessage(res, t("获取工作流列表成功!"), 'WorkflowServiceHandle_GetWorkFlowByIds')
} catch (error) {
return errorMessage(
t("获取工作流列表失败,{error}", {
error: (error as Error).message
}),
'WorkflowServiceHandle_GetWorkFlowByIds'
)
}
}
/**
*
* @param workflow nametypeworkflowFilePath等必要字段
* @returns
*/
AddWorkFlow = async (workflow: Partial<WorkflowModel.Workflow>): Promise<SuccessItem | ErrorItem> => {
try {
await this.InitSDBasic()
let res = this.workflowRealmService.AddWorkFlow(workflow)
return successMessage(res, t("添加工作流成功!"), 'WorkflowServiceHandle_AddWorkFlow')
} catch (error) {
return errorMessage(
t('添加工作流失败,{error}', {
error: (error as Error).message
}),
'WorkflowServiceHandle_AddWorkFlow'
)
}
}
/**
* ID的工作流
* @param id
* @param workflow
* @returns
*/
ModifyWorkflow = async (id: string, workflow: Partial<WorkflowModel.Workflow>): Promise<SuccessItem | ErrorItem> => {
try {
await this.InitSDBasic()
let res = this.workflowRealmService.ModifyWorkflow(id, workflow)
return successMessage(res, t("修改工作流成功!"), 'WorkflowServiceHandle_ModifyWorkflow')
} catch (error) {
return errorMessage(
t('修改工作流失败,{error}', {
error: (error as Error).message
}),
'WorkflowServiceHandle_ModifyWorkflow'
)
}
}
/**
* ID的工作流
* @param id
* @returns
*/
DeleteWorkflow = async (id: string): Promise<SuccessItem | ErrorItem> => {
try {
await this.InitSDBasic()
this.workflowRealmService.DeleteWorkflow(id)
return successMessage(null, t("删除工作流成功!"), 'WorkflowServiceHandle_DeleteWorkflow')
} catch (error) {
return errorMessage(
t('删除工作流失败,{error}', {
error: (error as Error).message
}),
'WorkflowServiceHandle_DeleteWorkflow'
)
}
}
/**
* ID对应的工作流
* @param ids ID数组
* @returns
*/
DeleteWorkflowByIds = async (ids: string[]): Promise<SuccessItem | ErrorItem> => {
try {
await this.InitSDBasic()
this.workflowRealmService.DeleteWorkflowByIds(ids)
return successMessage(null, t("批量删除工作流成功!"), 'WorkflowServiceHandle_DeleteWorkflowByIds')
} catch (error) {
return errorMessage(
t('批量删除工作流失败,{error}', {
error: (error as Error).message
}),
'WorkflowServiceHandle_DeleteWorkflowByIds'
)
}
}
}

View File

@ -443,6 +443,7 @@ export class TaskManager {
case BookBackTaskType.HAILUO_TEXT_TO_VIDEO:
case BookBackTaskType.HAILUO_IMAGE_TO_VIDEO:
case BookBackTaskType.HAILUO_FIRST_LAST_FRAME:
case BookBackTaskType.COMFYUI_VIDEO:
this.AddImageToVideo(task)
break

View File

@ -81,7 +81,7 @@ export class TranslateCommon {
return await this.TranslateReturnNowTencent(value)
} else if (this.translationBusiness.includes('aliyun')) {
return await this.TranslateReturnNowAliyun(value)
} else if (this.translationBusiness.includes('laitool')) {
} else if (this.translationBusiness.includes('laitool') || this.translationBusiness.includes('zhiluoai')) {
return await this.TranslateReturnNowGPT(value)
} else {
throw new Error('没有找到对应的翻译API')

View File

@ -2,16 +2,19 @@ import { TaskModal } from '@/define/model/task'
import { MJVideoService } from './mjVideo'
import { KlingVideoService } from './klingVideo'
import { HaiLuoVideoService } from './hailuoVideo'
import { SDHandle } from '../sd'
export class VideoHandle {
mjVideoService: MJVideoService
klingVideoService: KlingVideoService
hailuoVideoService: HaiLuoVideoService
sdHandle: SDHandle
// 这里可以添加 VideoHandle 特有的方法
constructor() {
// mixin 装饰器会处理初始化
this.mjVideoService = new MJVideoService()
this.klingVideoService = new KlingVideoService()
this.hailuoVideoService = new HaiLuoVideoService()
this.sdHandle = new SDHandle()
}
/** MJ图片转视频处理方法 将指定的图片通过Midjourney API转换为视频 */
@ -45,4 +48,8 @@ export class VideoHandle {
HailuoFirstLastFrameToVideo(task: TaskModal.Task) {
return this.hailuoVideoService.HailuoFirstLastFrameToVideo(task)
}
ComfyUIImageToVideo(task: TaskModal.Task) {
return this.sdHandle.ComfyUIVideoGenerate(task)
}
}

View File

@ -1,10 +1,72 @@
import { ipcRenderer } from 'electron'
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { WorkflowModel } from '@/define/model/workflow'
const setting = {
/** 获取默认的草稿保存路径 */
GetDefaultJianyingDraftPath: async () =>
ipcRenderer.invoke(DEFINE_STRING.SETTING.GET_DEFAULT_JIANYING_DRAFT_PATH)
ipcRenderer.invoke(DEFINE_STRING.SETTING.GET_DEFAULT_JIANYING_DRAFT_PATH),
//#region Workflow 工作流相关方法
/**
*
* @param condition idnametypeworkflowFilePathpagepageSize等
* @returns
*/
GetWorkFlowByCondition: async (condition: WorkflowModel.QueryWorkflowCondition) =>
ipcRenderer.invoke(DEFINE_STRING.SETTING.GET_WORKFLOW_BY_CONDITION, condition),
/**
* ID查询单个工作流
* @param id
* @returns null的成功或错误消息
*/
GetWorkFlowById: async (id: string) =>
ipcRenderer.invoke(DEFINE_STRING.SETTING.GET_WORKFLOW_BY_ID, id),
/**
* ID查询工作流
* @param ids ID数组
* @returns
*/
GetWorkFlowByIds: async (ids: string[]) =>
ipcRenderer.invoke(DEFINE_STRING.SETTING.GET_WORKFLOW_BY_IDS, ids),
/**
*
* @param workflow nametypeworkflowFilePath等必要字段
* @returns
*/
AddWorkFlow: async (workflow: WorkflowModel.Workflow) =>
ipcRenderer.invoke(DEFINE_STRING.SETTING.ADD_WORKFLOW, workflow),
/**
* ID的工作流
* @param id
* @param workflow
* @returns
*/
ModifyWorkflow: async (id: string, workflow: WorkflowModel.Workflow) =>
ipcRenderer.invoke(DEFINE_STRING.SETTING.MODIFY_WORKFLOW, id, workflow),
/**
* ID的工作流
* @param id
* @returns
*/
DeleteWorkflow: async (id: string) =>
ipcRenderer.invoke(DEFINE_STRING.SETTING.DELETE_WORKFLOW, id),
/**
* ID对应的工作流
* @param ids ID数组
* @returns
*/
DeleteWorkflowByIds: async (ids: string[]) =>
ipcRenderer.invoke(DEFINE_STRING.SETTING.DELETE_WORKFLOW_BY_IDS, ids)
//#endregion
}
export { setting }

View File

@ -19,6 +19,7 @@ declare module 'vue' {
BookTaskImageCache: typeof import('./src/components/Original/Image/BookTaskImageCache.vue')['default']
CharacterPreset: typeof import('./src/components/Preset/CharacterPreset.vue')['default']
ComfyUIAddWorkflow: typeof import('./src/components/Setting/ComfyUIAddWorkflow.vue')['default']
ComfyUIImageToVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfyUI/ComfyUIImageToVideoInfo.vue')['default']
ComfyUISetting: typeof import('./src/components/Setting/ComfyUISetting.vue')['default']
CommonDialog: typeof import('./src/components/common/CommonDialog.vue')['default']
ConfigOptionGroup: typeof import('./src/components/common/ConfigOptionGroup.vue')['default']
@ -37,6 +38,7 @@ declare module 'vue' {
DatatableHeaderCharacter: typeof import('./src/components/Original/BookTaskDetail/DatatableHeaderCharacter.vue')['default']
DatatableHeaderGptPrompt: typeof import('./src/components/Original/BookTaskDetail/DatatableHeaderGptPrompt.vue')['default']
DatatableHeaderImage: typeof import('./src/components/Original/BookTaskDetail/DatatableHeaderImage.vue')['default']
DialogTextContent: typeof import('./src/components/common/DialogTextContent.vue')['default']
DisabledWrapper: typeof import('./src/components/common/DisabledWrapper.vue')['default']
DocHelp: typeof import('./src/components/DocHelp.vue')['default']
DownloadRound: typeof import('./src/components/common/Icon/DownloadRound.vue')['default']
@ -61,6 +63,7 @@ declare module 'vue' {
LoadingComponent: typeof import('./src/components/common/LoadingComponent.vue')['default']
ManageAISetting: typeof import('./src/components/CopyWriting/ManageAISetting.vue')['default']
MediaToVideoInfoBasicInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue')['default']
MediaToVideoInfoComfyUIInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfyUI/MediaToVideoInfoComfyUIInfo.vue')['default']
MediaToVideoInfoConfig: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue')['default']
MediaToVideoInfoEmptyState: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoEmptyState.vue')['default']
MediaToVideoInfoHaiLuoVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoHaiLuo/MediaToVideoInfoHaiLuoVideoInfo.vue')['default']
@ -75,6 +78,7 @@ declare module 'vue' {
MediaToVideoInfoVideoConfig: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoConfig.vue')['default']
MediaToVideoInfoVideoListInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue')['default']
MediaToVideoSelectParentTask: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoSelectParentTask.vue')['default']
MediaToVideoVideoConfigHeader: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoVideoConfigHeader.vue')['default']
MenuOpenRound: typeof import('./src/components/common/Icon/MenuOpenRound.vue')['default']
MessageAndProgress: typeof import('./src/components/Original/BookTaskDetail/MessageAndProgress.vue')['default']
MJAccountDialog: typeof import('./src/components/Setting/MJSetting/MJAccountDialog.vue')['default']

View File

@ -5,6 +5,7 @@ import { JianyingKeyFrameEnum } from '@/define/enum/jianyingEnum'
import { OptionKeyName, OptionType } from '@/define/enum/option'
import { ImageToVideoModels } from '@/define/enum/video'
import { SettingModal } from '@/define/model/setting'
import { WorkflowModel } from '@/define/model/workflow'
import { ValidateJson, ValidateJsonAndParse } from '@/define/Tools/validate'
import { t } from '@/i18n'
import { optionSerialization } from '@/main/service/option/optionSerialization'
@ -522,9 +523,10 @@ export async function InitJianyingKeyFrameSetting() {
let defaultComfyuiSimpleSetting: SettingModal.ComfyUISimpleSettingModel = {
requestUrl: 'http://127.0.0.1:8188/',
selectedWorkflow: undefined,
imageToVideoSelectWorkflow : undefined,
negativePrompt: undefined
}
let defaultComfyuiWorkFlowSetting: Array<SettingModal.ComfyUIWorkFlowSettingModel> = []
let defaultComfyuiWorkFlowSetting: Array<WorkflowModel.Workflow> = []
/**
*

View File

@ -152,6 +152,11 @@
<MediaToVideoInfoMJVideoInfo :task="task" />
</div>
<!-- ComfyUI 类型 -->
<div v-else-if="selectedVideoType === ImageToVideoModels.COMFY_UI" class="info-content">
<MediaToVideoInfoComfyUIInfo :task="task" :workflow-list="workflowList" />
</div>
<!-- 未知类型或无数据 -->
<div v-else class="info-content">
<div class="empty-state">
@ -170,7 +175,7 @@
</template>
<script setup>
import { computed } from 'vue'
import { computed, ref, onMounted } from 'vue'
import { NCard, NTag, NIcon, NText, NSelect, useMessage } from 'naive-ui'
import {
GetImageToVideoModelsLabel,
@ -178,8 +183,15 @@ import {
ImageToVideoModels
} from '@/define/enum/video'
import MediaToVideoInfoMJVideoInfo from './MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoInfo.vue'
import MediaToVideoInfoKlingVideoInfo from './MediaToVideoInfoKling/MediaToVideoInfoKlingVideoInfo.vue'
import MediaToVideoInfoHaiLuoVideoInfo from './MediaToVideoInfoHaiLuo/MediaToVideoInfoHaiLuoVideoInfo.vue'
import MediaToVideoInfoComfyUIInfo from './MediaToVideoInfoConfyUI/MediaToVideoInfoComfyUIInfo.vue'
import { t } from '@/i18n'
import { ComfyUIWorkflowType } from '@/define/enum/comfyuiEnum'
import { useVideo } from '@/renderer/src/hooks/useVideo'
const { getComfyuiWorkflow } = useVideo()
const message = useMessage()
// props
@ -197,6 +209,20 @@ const selectedVideoType = computed(() => {
return props.task?.videoMessage?.videoType || ImageToVideoModels.MJ_VIDEO
})
// 使 ref 使 workflowList
const workflowList = ref([])
onMounted(async () => {
try {
const result = await getComfyuiWorkflow({ type: ComfyUIWorkflowType.IMAGE_TO_VIDEO })
workflowList.value = result || []
} catch (error) {
console.error('加载工作流列表失败:', error)
message.error(t('加载工作流列表失败:{error}', { error: error.message }))
workflowList.value = []
}
})
//
// const selectedVideoType = ref(videoType.value)

View File

@ -0,0 +1,169 @@
<template>
<n-space vertical :size="20" style="width: 100%">
<ConfigOptionGroup
v-model:value="videoMessage.comfyUIOptionsObject"
:options="comfyUIOptions"
@change="handleConfigChange"
/>
<div style="display: flex; gap: 12px; width: 100%">
<TooltipButton
:tooltip="t('将当前转视频的基础设置批量应用到所有的分镜中')"
type="default"
size="small"
@click="handleBatchSettings"
>
<template #icon>
<n-icon>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M3,6V8H21V6H3M3,11V13H21V11H3M3,16V18H21V16H3Z" />
</svg>
</n-icon>
</template>
{{ t('应用设置') }}
</TooltipButton>
<n-button type="primary" size="small" @click="handleImageToVideo" style="flex: 1">
<template #icon>
<n-icon>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M8,5.14V19.14L19,12.14L8,5.14Z" />
</svg>
</n-icon>
</template>
{{ t('生成视频') }}
</n-button>
</div>
</n-space>
</template>
<script setup>
import { computed } from 'vue'
import ConfigOptionGroup from '@/renderer/src/components/common/ConfigOptionGroup.vue'
import TooltipButton from '@/renderer/src/components/common/TooltipButton.vue'
import { t } from '@/i18n'
const props = defineProps({
task: {
type: Object,
required: true
},
videoMessage: {
type: Object,
required: true
},
workflowList: {
type: Array,
required: true
}
})
const emit = defineEmits(['update-comfyui-options', 'batch-settings', 'image-to-video'])
//
const handleConfigChange = (key, value) => {
//
emit('update-comfyui-options', key, value)
console.log('ComfyUI options changed:', key, value)
}
//
async function handleBatchSettings() {
emit('batch-settings')
}
//
async function handleImageToVideo() {
emit('image-to-video')
}
const comfyUIOptions = computed(() => [
{
key: 'workflow_file',
label: t('工作流'),
type: 'select',
options: props.workflowList,
placeholder: t('请选择 {data}', { data: t('工作流') }),
fullWidth: true,
required: true,
tooltip: t(
'<strong>必选</strong><br/><br/>在 <strong>设置 -> Comfyui 设置</strong> 中设置的类型为 <strong>图转视频</strong> 的工作流'
)
},
{
key: 'first_frame_image',
label: t('首帧参考图像'),
type: 'image',
placeholder: t('请输入 {data}', { data: t('图片地址') }),
fullWidth: true,
required: false,
tooltip: t(
'<strong>必选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 作为视频生成的起始帧参考<br/>• 本地文件路径'
)
},
{
key: 'last_frame_image',
label: t('尾帧参考图像'),
type: 'image',
fullWidth: true,
required: false,
placeholder: t('请输入 {data}', { data: t('图片地址') }),
tooltip: t(
'<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 作为视频生成的起始帧参考<br/>• 本地文件路径'
)
},
{
key: 'prompt',
label: t('正向提示词'),
type: 'input',
inputType: 'textarea',
autosize: { minRows: 3, maxRows: 3 },
fullWidth: true,
placeholder: t('请输入 {data}', { data: t('正向提示词') }),
tooltip: t('描述期望生成的视频内容')
},
{
key: 'negative_prompt',
label: t('反向提示词'),
type: 'input',
inputType: 'textarea',
autosize: { minRows: 3, maxRows: 3 },
fullWidth: true,
placeholder: t('请输入 {data}', { data: t('反向提示词') }),
tooltip: t('描述不希望出现在视频中的内容')
},
{
key: 'resolution',
label: t('视频分辨率'),
type: 'number',
min: 128,
max: 4096,
step: 1,
placeholder: t('例如: 1024'),
width: '150px',
tooltip: t('必填<br/><br/>生成视频的像素分辨率<br/><br/>常用值:<strong>512, 768, 1024, 1920</strong>')
},
{
key: 'duration',
label: t('视频时长(秒)'),
type: 'number',
min: 1,
max: 60,
step: 1,
placeholder: t('例如: 5'),
width: '120px',
tooltip: t('必填<br/><br/>生成视频的持续时间(秒)<br/><br/>初始值会根据分镜时长生成')
},
// {
// key: 'fps',
// label: t('(FPS)'),
// type: 'number',
// min: 1,
// max: 60,
// step: 1,
// placeholder: t(': 24'),
// width: '120px',
// tooltip: t('<br/><br/><strong>24, 30, 60</strong>')
// }
])
</script>

View File

@ -0,0 +1,280 @@
<template>
<div class="comfyui-video-container">
<ComfyUIImageToVideoInfo
:task="props.task"
:video-message="videoMessage"
:workflow-list="workflowList"
@update-comfyui-options="handleComfyUIOptionsUpdate"
@batch-settings="handleBatchSettings"
@image-to-video="handleImageToVideo"
/>
</div>
</template>
<script setup>
import { computed, h } from 'vue'
import { useMessage, useDialog } from 'naive-ui'
import ComfyUIImageToVideoInfo from './ComfyUIImageToVideoInfo.vue'
import { t } from '@/i18n'
import { ValidateJsonAndParse } from '@/define/Tools/validate'
import { useSoftwareStore, useBookStore } from '@/renderer/src/stores'
import { isEmpty } from 'lodash'
import { BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { AddOneTask } from '@/renderer/src/common/task'
const message = useMessage()
const dialog = useDialog()
const softwareStore = useSoftwareStore()
const bookStore = useBookStore()
const props = defineProps({
task: {
type: Object,
required: true
},
workflowList: {
type: Array,
required: true
}
})
// const workflowList = computed(() => props.workflowList)
//
const videoMessage = computed(() => {
let videoMessage = props.task?.videoMessage || {}
let comfyUIOptionsString = videoMessage.comfyUIOptions || '{}'
let comfyUIOptions = ValidateJsonAndParse(comfyUIOptionsString)
// comfyUIOptions
let cleanComfyUIOptions = {
workflow_file: comfyUIOptions.workflow_file || '',
first_frame_image: videoMessage.imageUrl || comfyUIOptions.first_frame_image || '',
last_frame_image: comfyUIOptions.last_frame_image || '',
prompt: videoMessage.prompt || '',
negative_prompt: comfyUIOptions.negative_prompt || '',
resolution: comfyUIOptions.resolution || 768,
duration: comfyUIOptions.duration || 5,
fps: comfyUIOptions.fps || 24
}
if (isEmpty(cleanComfyUIOptions.workflow_file)) {
cleanComfyUIOptions.workflow_file =
props.workflowList?.length > 0 ? props.workflowList[0].value : ''
}
videoMessage.comfyUIOptionsObject = cleanComfyUIOptions
console.log(
'MediaToVideoInfoComfyUIInfo videoMessage',
videoMessage,
videoMessage.comfyUIOptionsObject
)
return videoMessage
})
// ComfyUI
async function handleComfyUIOptionsUpdate(key, value = undefined) {
let updateObject = {}
switch (key) {
case 'workflow_file':
updateObject.comfyUIOptions = JSON.stringify({
...videoMessage.value.comfyUIOptionsObject,
workflow_file: value
})
break
case 'first_frame_image':
updateObject.imageUrl = value
updateObject.comfyUIOptions = JSON.stringify({
...videoMessage.value.comfyUIOptionsObject,
first_frame_image: value
})
break
case 'last_frame_image':
updateObject.comfyUIOptions = JSON.stringify({
...videoMessage.value.comfyUIOptionsObject,
last_frame_image: value
})
break
case 'prompt':
updateObject.prompt = value
updateObject.comfyUIOptions = JSON.stringify({
...videoMessage.value.comfyUIOptionsObject,
prompt: value
})
break
case 'negative_prompt':
updateObject.negative_prompt = value
updateObject.comfyUIOptions = JSON.stringify({
...videoMessage.value.comfyUIOptionsObject,
negative_prompt: value
})
break
case 'resolution':
updateObject.comfyUIOptions = JSON.stringify({
...videoMessage.value.comfyUIOptionsObject,
resolution: value
})
break
case 'duration':
updateObject.comfyUIOptions = JSON.stringify({
...videoMessage.value.comfyUIOptionsObject,
duration: value
})
break
case 'fps':
updateObject.comfyUIOptions = JSON.stringify({
...videoMessage.value.comfyUIOptionsObject,
fps: value
})
break
default:
message.error(t('未知的修改键: {key}', { key }))
return
}
//
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(props.task.id, updateObject)
if (res.code !== 1) {
message.error(
t('保存失败:{error}', {
error: res.message
}) + `, Key: ${key}`
)
return
}
// tab
if (!props.task.videoMessage) {
props.task.videoMessage = {}
}
Object.assign(props.task.videoMessage, updateObject)
// comfyUIOptionsObject
if (updateObject.comfyUIOptions) {
const updatedOptions = ValidateJsonAndParse(updateObject.comfyUIOptions)
videoMessage.value.comfyUIOptionsObject = {
...videoMessage.value.comfyUIOptionsObject,
...updatedOptions
}
}
}
//
async function handleBatchSettings() {
let da = dialog.warning({
title: t('操作确认'),
content: () =>
h(
'div',
{
style: {
whiteSpace: 'pre-line'
}
},
{
default: () =>
t(
'是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置工作流文件、分辨率、时长、帧率等基础设置\n\n批量应用后其余分镜的上述基础设置会被替换为当前分镜的数据是否继续'
)
}
),
positiveText: t('确认'),
negativeText: t('取消'),
closable: true,
onPositiveClick: async () => {
da.destroy()
try {
softwareStore.spin.spinning = true
softwareStore.spin.tip = t('正在批量应用当前设置...')
//
for (let i = 0; i < bookStore.selectBookTaskDetail.length; i++) {
const element = bookStore.selectBookTaskDetail[i]
let updateObject = {}
//
let elementVideoMessage = element?.videoMessage || {}
let elementComfyUIOptionsString = elementVideoMessage.comfyUIOptions || '{}'
let elementComfyUIOptions = ValidateJsonAndParse(elementComfyUIOptionsString)
// ComfyUI
elementComfyUIOptions.workflow_file =
videoMessage.value.comfyUIOptionsObject.workflow_file || ''
elementComfyUIOptions.resolution = videoMessage.value.comfyUIOptionsObject.resolution
elementComfyUIOptions.duration = videoMessage.value.comfyUIOptionsObject.duration
elementComfyUIOptions.fps = videoMessage.value.comfyUIOptionsObject.fps
elementVideoMessage.comfyUIOptions = JSON.stringify(elementComfyUIOptions)
updateObject.comfyUIOptions = elementVideoMessage.comfyUIOptions
//
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(
element.id,
updateObject
)
if (res.code !== 1) {
message.error(
t('批量应用当前设置失败,{error}', {
error: res.message
})
)
return
}
}
message.success(t('批量应用当前设置成功!'))
} catch (error) {
message.error(
t('批量应用当前设置失败,{error}', {
error: error.message
})
)
} finally {
softwareStore.spin.spinning = false
}
},
onNegativeClick: () => {
da.destroy()
message.info(t('取消操作'))
}
})
}
//
async function handleImageToVideo() {
if (isEmpty(videoMessage.value.comfyUIOptionsObject.workflow_file)) {
message.error(
t('请选择 {data}', {
data: t('工作流')
})
)
return
}
let res = await AddOneTask({
bookId: props.task.bookId,
type: BookBackTaskType.COMFYUI_VIDEO,
executeType: TaskExecuteType.AUTO,
bookTaskId: props.task.bookTaskId,
bookTaskDetailId: props.task.id,
messageName: DEFINE_STRING.BOOK.COMFYUI_TO_VIDEO_RETURN
})
if (res.code != 1) {
message.error(res.message)
return
}
message.success(res.message)
}
</script>
<style scoped>
.comfyui-video-container {
width: 100%;
}
</style>

View File

@ -12,7 +12,6 @@
type="default"
size="small"
@click="handleBatchSettings"
style="width: 100px"
>
<template #icon>
<n-icon>

View File

@ -12,7 +12,6 @@
type="default"
size="small"
@click="handleBatchSettings"
style="width: 100px"
>
<template #icon>
<n-icon>

View File

@ -12,7 +12,7 @@
type="default"
size="small"
@click="handleBatchSettings"
style="width: 100px"
>
<template #icon>
<n-icon>
@ -300,107 +300,96 @@ onMounted(() => {
})
//
const handleConfigChange = (key, value, newValue) => {
const handleConfigChange = (key, value) => {
//
if (key === 'model') {
const updatedValue = { ...newValue }
const currentOptions = props.videoMessage.hailuoTextToVideoOptionsObject
//
const supportedResolutions = GetHailuoModelSupportedResolutions(
'textToVideo',
value,
updatedValue.duration
currentOptions.duration
)
const currentResolutionSupported = supportedResolutions.some(
(option) => option.value === updatedValue.resolution
(option) => option.value === currentOptions.resolution
)
if (!currentResolutionSupported) {
//
if (value === HailuoModel.MINIMAX_HAILUO_02) {
updatedValue.resolution = HailuoResolution.P768 // MiniMax-Hailuo-02 768P
} else {
updatedValue.resolution = HailuoResolution.P720 // 720P
}
const defaultResolution = value === HailuoModel.MINIMAX_HAILUO_02
? HailuoResolution.P768
: HailuoResolution.P720
emit('update-hailuo-options', 'textToVideo', 'resolution', defaultResolution)
}
//
const supportedDurations = GetHailuoModelSupportedDurations(
value,
updatedValue.resolution,
currentOptions.resolution,
'textToVideo'
)
const currentDurationSupported = supportedDurations.some(
(option) => option.value === updatedValue.duration
(option) => option.value === currentOptions.duration
)
if (!currentDurationSupported) {
// 6
updatedValue.duration = HailuoDuration.SIX
emit('update-hailuo-options', 'textToVideo', 'duration', HailuoDuration.SIX)
}
// MiniMax-Hailuo-02
if (value !== HailuoModel.MINIMAX_HAILUO_02) {
updatedValue.fast_pretreatment = false
if (value !== HailuoModel.MINIMAX_HAILUO_02 && currentOptions.fast_pretreatment) {
emit('update-hailuo-options', 'textToVideo', 'fast_pretreatment', false)
}
emit('update-hailuo-options', 'textToVideo', key, value, updatedValue)
return
}
//
if (key === 'resolution') {
const updatedValue = { ...newValue }
const currentOptions = props.videoMessage.hailuoTextToVideoOptionsObject
//
const supportedDurations = GetHailuoModelSupportedDurations(
updatedValue.model,
currentOptions.model,
value,
'textToVideo'
)
const currentDurationSupported = supportedDurations.some(
(option) => option.value === updatedValue.duration
(option) => option.value === currentOptions.duration
)
if (!currentDurationSupported) {
// 6
updatedValue.duration = HailuoDuration.SIX
emit('update-hailuo-options', 'textToVideo', 'duration', HailuoDuration.SIX)
}
emit('update-hailuo-options', 'textToVideo', key, value, updatedValue)
return
}
//
if (key === 'duration') {
const updatedValue = { ...newValue }
const currentOptions = props.videoMessage.hailuoTextToVideoOptionsObject
//
const supportedResolutions = GetHailuoModelSupportedResolutions(
'textToVideo',
updatedValue.model,
currentOptions.model,
value
)
const currentResolutionSupported = supportedResolutions.some(
(option) => option.value === updatedValue.resolution
(option) => option.value === currentOptions.resolution
)
if (!currentResolutionSupported) {
//
if (updatedValue.model === HailuoModel.MINIMAX_HAILUO_02) {
updatedValue.resolution = HailuoResolution.P768 // MiniMax-Hailuo-02 768P
} else {
updatedValue.resolution = HailuoResolution.P720 // 720P
const defaultResolution = currentOptions.model === HailuoModel.MINIMAX_HAILUO_02
? HailuoResolution.P768
: HailuoResolution.P720
emit('update-hailuo-options', 'textToVideo', 'resolution', defaultResolution)
}
}
emit('update-hailuo-options', 'textToVideo', key, value, updatedValue)
return
}
//
emit('update-hailuo-options', 'textToVideo', key, value, newValue)
console.log('Hailuo text-to-video options changed:', key, value, newValue)
//
emit('update-hailuo-options', 'textToVideo', key, value)
console.log('Hailuo text-to-video options changed:', key, value)
}
//

View File

@ -87,7 +87,7 @@ const videoMessage = computed(() => {
//
const cleanTextToVideoOptions = {
model: hailuoTextToVideoOptions.model || HailuoModel.MINIMAX_HAILUO_02,
prompt: hailuoTextToVideoOptions.prompt || '',
prompt: videoMessage.prompt || '',
duration: hailuoTextToVideoOptions.duration ?? HailuoDuration.SIX,
resolution: hailuoTextToVideoOptions.resolution || HailuoResolution.P768,
prompt_optimizer: hailuoTextToVideoOptions.prompt_optimizer ?? true,
@ -98,7 +98,7 @@ const videoMessage = computed(() => {
const cleanFirstFrameOptions = {
model: hailuoFirstFrameOptions.model || HailuoModel.MINIMAX_HAILUO_02,
first_frame_image: videoMessage.imageUrl || hailuoFirstFrameOptions.first_frame_image || '',
prompt: hailuoFirstFrameOptions.prompt || '',
prompt: videoMessage.prompt || '',
duration: hailuoFirstFrameOptions.duration ?? HailuoDuration.SIX,
resolution: hailuoFirstFrameOptions.resolution || HailuoResolution.P768,
prompt_optimizer: hailuoFirstFrameOptions.prompt_optimizer ?? true,
@ -110,7 +110,7 @@ const videoMessage = computed(() => {
model: hailuoFirstLastFrameOptions.model || HailuoModel.MINIMAX_HAILUO_02,
first_frame_image: videoMessage.imageUrl || hailuoFirstLastFrameOptions.first_frame_image || '',
last_frame_image: hailuoFirstLastFrameOptions.last_frame_image || '',
prompt: hailuoFirstLastFrameOptions.prompt || '',
prompt: videoMessage.prompt || '',
duration: HailuoDuration.SIX, // 6API
resolution: hailuoFirstLastFrameOptions.resolution || HailuoResolution.P768,
prompt_optimizer: hailuoFirstLastFrameOptions.prompt_optimizer ?? true,
@ -126,25 +126,115 @@ const videoMessage = computed(() => {
})
//
async function handleHailuoOptionsUpdate(optionsType, key, value, newOptions) {
//
let updateData = {
imageUrl: newOptions.first_frame_image || videoMessage.value.imageUrl
}
//
async function handleHailuoOptionsUpdate(optionsType, key, value = undefined) {
let updateObject = {}
//
switch (optionsType) {
case 'textToVideo':
updateData.hailuoTextToVideoOptions = JSON.stringify(newOptions)
case 'textToVideo': {
const currentOptions = videoMessage.value.hailuoTextToVideoOptionsObject
switch (key) {
case 'prompt':
updateObject.prompt = value
updateObject.hailuoTextToVideoOptions = JSON.stringify({
...currentOptions,
prompt: value
})
break
case 'firstFrameOnly':
updateData.hailuoFirstFrameOnlyOptions = JSON.stringify(newOptions)
case 'model':
case 'duration':
case 'resolution':
case 'prompt_optimizer':
case 'fast_pretreatment':
updateObject.hailuoTextToVideoOptions = JSON.stringify({
...currentOptions,
[key]: value
})
break
case 'firstLastFrame':
updateData.hailuoFirstLastFrameOptions = JSON.stringify(newOptions)
default:
message.error(t('未知的修改键: {key}', { key }))
return
}
break
}
case 'firstFrameOnly': {
const currentOptions = videoMessage.value.hailuoFirstFrameOptionsObject
switch (key) {
case 'first_frame_image':
updateObject.imageUrl = value
updateObject.hailuoFirstFrameOnlyOptions = JSON.stringify({
...currentOptions,
first_frame_image: value
})
break
case 'prompt':
updateObject.prompt = value
updateObject.hailuoFirstFrameOnlyOptions = JSON.stringify({
...currentOptions,
prompt: value
})
break
case 'model':
case 'duration':
case 'resolution':
case 'prompt_optimizer':
case 'fast_pretreatment':
updateObject.hailuoFirstFrameOnlyOptions = JSON.stringify({
...currentOptions,
[key]: value
})
break
default:
message.error(t('未知的修改键: {key}', { key }))
return
}
break
}
case 'firstLastFrame': {
const currentOptions = videoMessage.value.hailuoFirstLastFrameOptionsObject
switch (key) {
case 'first_frame_image':
updateObject.imageUrl = value
updateObject.hailuoFirstLastFrameOptions = JSON.stringify({
...currentOptions,
first_frame_image: value
})
break
case 'last_frame_image':
updateObject.hailuoFirstLastFrameOptions = JSON.stringify({
...currentOptions,
last_frame_image: value
})
break
case 'prompt':
updateObject.prompt = value
updateObject.hailuoFirstLastFrameOptions = JSON.stringify({
...currentOptions,
prompt: value
})
break
case 'model':
case 'resolution':
case 'prompt_optimizer':
case 'fast_pretreatment':
updateObject.hailuoFirstLastFrameOptions = JSON.stringify({
...currentOptions,
[key]: value
})
break
default:
message.error(t('未知的修改键: {key}', { key }))
return
}
break
}
default:
message.error(t('未知的配置类型: {type}', { type: optionsType }))
return
}
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(props.task.id, updateData)
//
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(props.task.id, updateObject)
if (res.code !== 1) {
message.error(
t('保存失败:{error}', {
@ -159,19 +249,29 @@ async function handleHailuoOptionsUpdate(optionsType, key, value, newOptions) {
props.task.videoMessage = {}
}
Object.assign(props.task.videoMessage, updateData)
Object.assign(props.task.videoMessage, updateObject)
//
switch (optionsType) {
case 'textToVideo':
videoMessage.value.hailuoTextToVideoOptionsObject = { ...newOptions }
break
case 'firstFrameOnly':
videoMessage.value.hailuoFirstFrameOptionsObject = { ...newOptions }
break
case 'firstLastFrame':
videoMessage.value.hailuoFirstLastFrameOptionsObject = { ...newOptions }
break
if (updateObject.hailuoTextToVideoOptions) {
const updatedOptions = ValidateJsonAndParse(updateObject.hailuoTextToVideoOptions)
videoMessage.value.hailuoTextToVideoOptionsObject = {
...videoMessage.value.hailuoTextToVideoOptionsObject,
...updatedOptions
}
}
if (updateObject.hailuoFirstFrameOnlyOptions) {
const updatedOptions = ValidateJsonAndParse(updateObject.hailuoFirstFrameOnlyOptions)
videoMessage.value.hailuoFirstFrameOptionsObject = {
...videoMessage.value.hailuoFirstFrameOptionsObject,
...updatedOptions
}
}
if (updateObject.hailuoFirstLastFrameOptions) {
const updatedOptions = ValidateJsonAndParse(updateObject.hailuoFirstLastFrameOptions)
videoMessage.value.hailuoFirstLastFrameOptionsObject = {
...videoMessage.value.hailuoFirstLastFrameOptionsObject,
...updatedOptions
}
}
}

View File

@ -12,7 +12,7 @@
type="default"
size="small"
@click="handleBatchSettings"
style="width: 100px"
>
<template #icon>
<n-icon>
@ -66,10 +66,10 @@ const props = defineProps({
const emit = defineEmits(['update-kling-options', 'batch-settings', 'image-to-video'])
//
const handleConfigChange = (key, value, newValue) => {
const handleConfigChange = (key, value) => {
//
emit('update-kling-options', key, value, newValue)
console.log('Kling options changed:', key, value, newValue)
emit('update-kling-options', key, value)
console.log('Kling options changed:', key, value)
}
//
@ -87,12 +87,8 @@ async function handleImageUpload(key, imagePath) {
const url = await UploadImageToLaiTool(imagePath, 'video')
if (url) {
const newValue = {
...props.videoMessage.klingOptionsObject,
[key]: url
}
//
emit('update-kling-options', key, url, newValue)
emit('update-kling-options', key, url)
}
}

View File

@ -112,7 +112,7 @@ const klingOptions = computed(() => [
])
//
function handleConfigChange(key, value, newValue) {
emit('update-kling-options', key, value, newValue)
function handleConfigChange(key, value) {
emit('update-kling-options', key, value)
}
</script>

View File

@ -74,7 +74,8 @@ const subVideoPathObject = computed(() => {
return props.task?.subVideoPathObject.filter(
(video) =>
!isEmpty(video.localPath) &&
(video.type == ImageToVideoModels.KLING || video.type == ImageToVideoModels.KLING_VIDEO_EXTEND)
(video.type == ImageToVideoModels.KLING ||
video.type == ImageToVideoModels.KLING_VIDEO_EXTEND)
)
})
@ -97,7 +98,7 @@ const videoMessage = computed(() => {
model_name: klingVideoOptions.model_name || KlingModelName.KLING_V2_1,
image: videoMessage.imageUrl || klingVideoOptions.image || '',
image_tail: klingVideoOptions.image_tail || '',
prompt: klingVideoOptions.prompt || '',
prompt: videoMessage.prompt,
negative_prompt: klingVideoOptions.negative_prompt || '',
cfg_scale: klingVideoOptions.cfg_scale ?? 0.5,
mode: klingVideoOptions.mode || KlingMode.STD,
@ -117,13 +118,82 @@ const videoMessage = computed(() => {
})
// Kling
async function handleKlingOptionsUpdate(key, value, newOptions) {
//
let updateData = {
klingOptions: JSON.stringify(newOptions),
imageUrl: newOptions.image // imageUrl
async function handleKlingOptionsUpdate(key, value = undefined) {
let updateObject = {}
switch (key) {
case 'imageUrl':
case 'image':
updateObject.imageUrl = value
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
image: value
})
break
case 'image_tail':
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
image_tail: value
})
break
case 'prompt':
updateObject.prompt = value
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
prompt: value
})
break
case 'negative_prompt':
updateObject.negative_prompt = value
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
negative_prompt: value
})
break
case 'cfg_scale':
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
cfg_scale: value
})
break
case 'mode':
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
mode: value
})
break
case 'duration':
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
duration: value
})
break
case 'model_name':
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
model_name: value
})
break
case 'video_id':
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
video_id: value
})
break
case 'task_id':
updateObject.klingOptions = JSON.stringify({
...videoMessage.value.klingOptionsObject,
task_id: value
})
break
default:
message.error(t('未知的修改键: {key}', { key }))
return
}
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(props.task.id, updateData)
debugger
//
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(props.task.id, updateObject)
if (res.code !== 1) {
message.error(
t('保存失败:{error}', {
@ -138,12 +208,15 @@ async function handleKlingOptionsUpdate(key, value, newOptions) {
props.task.videoMessage = {}
}
Object.assign(props.task.videoMessage, updateData)
Object.assign(props.task.videoMessage, updateObject)
// klingOptionsObject
if (updateObject.klingOptions) {
const updatedKlingOptions = ValidateJsonAndParse(updateObject.klingOptions)
videoMessage.value.klingOptionsObject = {
...videoMessage.value.klingOptionsObject,
...newOptions
...updatedKlingOptions
}
}
}
//
@ -262,22 +335,17 @@ async function handleSelectParentTask() {
//
async function handleParentTaskSelection(selectedVideoInfo) {
try {
// Kling video_id
const currentKlingOptions = videoMessage.value.klingOptionsObject
const updatedKlingOptions = {
...currentKlingOptions,
video_id: selectedVideoInfo.videoId,
task_id: selectedVideoInfo.taskId
}
// video_id
await handleKlingOptionsUpdate('video_id', selectedVideoInfo.videoId)
//
await handleKlingOptionsUpdate('video_id', selectedVideoInfo.taskId, updatedKlingOptions)
// task_id
await handleKlingOptionsUpdate('task_id', selectedVideoInfo.taskId)
// modal
showParentTaskModal.value = false
message.success(
t('父任务选择成功视频ID已更新为: {videoId}', { videoId: selectedVideoInfo.taskId })
t('父任务选择成功视频ID已更新为: {videoId}', { videoId: selectedVideoInfo.videoId })
)
} catch (error) {
message.error(t('保存失败:{error}', { error: error.message }))

View File

@ -126,7 +126,7 @@
placeholder="0-3"
size="small"
:disabled="loading"
style="width: 100px"
@update:value="(value) => handleVideoMessageChange('index', value)"
/>
</div>
@ -450,7 +450,6 @@
size="small"
:loading="loading"
@click="handleBatchSettings"
style="width: 100px"
>
<template #icon>
<n-icon>

View File

@ -16,7 +16,6 @@
:tooltip="t('上传图片到LaiTool图床获取图片链接')"
quaternary
@click="handleUploadImage(videoMessage.imageUrl, 'video', 'imageUrl')"
>
<template #icon>
<n-icon size="20">
@ -51,9 +50,6 @@
class="preview-image"
@error="handleImageError(videoMessage.imageUrl)"
/>
<div v-else class="preview-placeholder">
<n-text depth="3" class="placeholder-text">{{ t('图片预览') }}</n-text>
</div>
</div>
</n-form-item>
@ -115,9 +111,6 @@
class="preview-image"
@error="handleImageError(videoMessage.mjVideoOptionsObject.endImageUrl)"
/>
<div v-else class="preview-placeholder">
<n-text depth="3" class="placeholder-text">{{ t('图片预览') }}</n-text>
</div>
</div>
</n-form-item>
@ -404,7 +397,6 @@
type="default"
size="small"
@click="handleBatchSettings"
style="width: 100px"
>
<template #icon>
<n-icon>
@ -416,12 +408,7 @@
{{ t('应用设置') }}
</TooltipButton>
<n-button
type="primary"
size="small"
@click="handleImageToVideo"
style="flex: 1"
>
<n-button type="primary" size="small" @click="handleImageToVideo" style="flex: 1">
<template #icon>
<n-icon>
<svg viewBox="0 0 24 24">

View File

@ -67,12 +67,16 @@ import ImageTextVideoInfoVideoConfig from './MediaToVideoInfoVideoConfig.vue'
import ImageTextVideoInfoVideoListInfo from './MediaToVideoInfoVideoListInfo.vue'
import ImageTextVideoInfoTaskOptions from './MediaToVideoInfoTaskOptions.vue'
import VideoDisplay from '@/renderer/src/components/common/VideoDisplay.vue'
import MediaToVideoVideoConfigHeader from './MediaToVideoVideoConfigHeader.vue'
import { OptionKeyName, OptionType } from '@/define/enum/option'
import { useBookStore } from '@/renderer/src/stores'
import { useBookStore, useSoftwareStore } from '@/renderer/src/stores'
import { GetImageToVideoModelsOptions } from '@/define/enum/video'
import { t } from '@/i18n'
import { TimeDelay } from '@/define/Tools/time'
import { isEmpty } from 'lodash'
const bookStore = useBookStore()
const softwareStore = useSoftwareStore()
const message = useMessage()
const dialog = useDialog()
@ -209,7 +213,7 @@ const columns = [
},
[
h(NImage, {
src: row.outImagePath,
src: row.outImagePath + '?t=' + new Date().getTime(),
height: 130,
width: 160,
objectFit: 'contain',
@ -248,28 +252,11 @@ const columns = [
},
{
title: () =>
h(
'div',
{
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '16px'
}
},
[
h('span', t('视频配置')),
h(NSelect, {
h(MediaToVideoVideoConfigHeader, {
value: batchVideoType.value,
size: 'small',
style: { width: '120px' },
options: videoTypeOptions,
placeholder: t('批量设置'),
onUpdateValue: handleBatchVideoTypeChange
})
]
),
onChange: handleBatchVideoTypeChange,
onActionSelect: handleActionDropdownSelect
}),
key: 'videoConfig',
minWidth: 300,
className: noPaddingColumnClass,
@ -410,6 +397,159 @@ async function handleBatchVideoTypeChange(value) {
}
}
//
async function handleImportTxtPrompt() {
let da = dialog.warning({
title: t('操作确认'),
content: () =>
h(
'div',
{ style: { whiteSpace: 'pre-line' } },
{
default: () =>
t(
`该操作会选择 TXT 文件进行导入提示词,\n\n提示词文件格式要求\n每行一个提示词顺序和当前分镜顺序一致\n如果某个分镜不需要导入提示词可以留空该行\n超出分镜的提示词会被删除不足则只导入文本中有的提示词数据\n\n是否继续`
)
}
),
negativeText: t('取消'),
positiveText: t('继续'),
onPositiveClick: async () => {
da?.destroy()
await TimeDelay(200)
try {
softwareStore.spin.spinning = true
softwareStore.spin.tip = t('正在导入提示词...')
//
let fileRes = await window.system.SelectSingleFile(['txt'])
if (fileRes.code == 0) {
throw new Error(fileRes.message)
}
let filePath = fileRes.data
//
let fileContentRes = await window.system.ReadTextFile(filePath)
if (fileContentRes.code == 0) {
throw new Error(fileContentRes.message)
}
let fileContent = fileContentRes.data.content
if (fileContent == null || fileContent == undefined || isEmpty(fileContent.trim())) {
throw new Error(t('导入的提示词文件内容为空'))
}
let lines = fileContent.split(/\r?\n/).map((line) => line.trim())
//
for (let i = 0; i < lines.length && i < bookStore.selectBookTaskDetail.length; i++) {
const element = lines[i]
const row = bookStore.selectBookTaskDetail[i]
//
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(row.id, {
prompt: element
})
if (res.code != 1) {
throw new Error(
t('导入第 {line} 行提示词失败,{error}', { line: i + 1, error: res.message })
)
}
//
bookStore.selectBookTaskDetail[i].videoMessage.prompt = element
}
await TimeDelay(500)
message.success(t('导入提示词成功'))
} catch (error) {
message.error(t('导入提示词失败,{error}', { error: error.message }))
} finally {
da?.destroy()
softwareStore.spin.spinning = false
}
},
onNegativeClick: () => {
message.info(t('取消操作'))
},
closable: true,
maskClosable: false
})
}
//
async function handleSyncImagePrompts() {
let da = dialog.warning({
title: t('操作确认'),
content: () =>
h(
'div',
{ style: { whiteSpace: 'pre-line' } },
{
default: () =>
t(
`该操作会同步生图提示词到图转视频的提示词,若不存在生图提示词则跳过当前分镜,同步操作不可逆!\n\n是否继续`
)
}
),
negativeText: t('取消'),
positiveText: t('继续'),
onPositiveClick: async () => {
da?.destroy()
await TimeDelay(200)
try {
softwareStore.spin.spinning = true
softwareStore.spin.tip = t('正在同步提示词...')
//
for (let i = 0; i < bookStore.selectBookTaskDetail.length; i++) {
const row = bookStore.selectBookTaskDetail[i]
let gptPrompt = row.gptPrompt ?? ''
if (isEmpty(gptPrompt)) {
continue //
}
//
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(row.id, {
prompt: gptPrompt
})
if (res.code != 1) {
throw new Error(
t('同步第 {line} 行提示词失败,{error}', { line: i + 1, error: res.message })
)
}
//
bookStore.selectBookTaskDetail[i].videoMessage.prompt = gptPrompt
}
await TimeDelay(500)
message.success(t('同步提示词成功!'))
} catch (error) {
message.error(t('同步提示词失败,{error}', { error: error.message }))
} finally {
da?.destroy()
softwareStore.spin.spinning = false
}
},
onNegativeClick: () => {
message.info(t('取消操作'))
},
closable: true,
maskClosable: false
})
}
//
async function handleActionDropdownSelect(key) {
switch (key) {
case 'import-prompts':
await handleImportTxtPrompt()
break
case 'sync-image-prompts':
await handleSyncImagePrompts()
break
default:
message.error(t('未知操作'))
break
}
}
//
async function handleSaveBookTaskDetailVideoMessage(row, taskId, key, value) {
try {

View File

@ -0,0 +1,110 @@
<template>
<div class="batch-video-type-selector">
<span class="title">{{ t('视频配置') }}</span>
<n-select
v-model:value="selectedType"
size="small"
:options="videoTypeOptions"
:placeholder="t('批量设置')"
class="selector"
@update:value="handleTypeChange"
/>
<TooltipDropdown
trigger="click"
:options="blockOptions"
@select="dropdownSelectHandle"
@update:show="dropdownVisible = $event"
>
<n-button size="tiny" tertiary type="primary" :style="style" class="dropdown-trigger">
<template #icon>
<n-icon size="18" class="trigger-icon">
<chevron-down v-if="!dropdownVisible" />
<chevron-up v-else />
</n-icon>
</template>
{{ t('更多操作') }}
</n-button>
</TooltipDropdown>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { NSelect } from 'naive-ui'
import { GetImageToVideoModelsOptions } from '@/define/enum/video'
import { t } from '@/i18n'
import { ChevronDown, ChevronUp } from '@vicons/ionicons5'
// emits
const emit = defineEmits(['update:value', 'change', 'action-select'])
const dropdownVisible = ref(false)
// props
const props = defineProps({
value: {
type: String,
default: null
},
style: {
type: Object,
default: () => ({})
}
})
const style = ref(props.style)
//
const selectedType = ref(props.value)
//
const videoTypeOptions = GetImageToVideoModelsOptions()
//
const handleTypeChange = (value) => {
selectedType.value = value
emit('update:value', value)
emit('change', value)
}
//
const blockOptions = computed(() => {
const baseOptions = [
{
label: t('导入提示词'),
tooltip: t('选择一个文本文件,导入其中的提示词,按行分割,依次应用到所有的分镜中。'),
key: 'import-prompts'
},
{
label: t('同步生图提示词'),
tooltip: t('同步当前分镜的生图提示词到图转视频的提示词中。'),
key: 'sync-image-prompts'
}
]
return baseOptions
})
//
async function dropdownSelectHandle(key) {
emit('action-select', key)
}
</script>
<style scoped>
.batch-video-type-selector {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.title {
font-weight: 500;
white-space: nowrap;
}
.selector {
width: 120px;
}
</style>

View File

@ -137,6 +137,7 @@ onUnmounted(() => {
window.system.removeEventListen([DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN])
window.system.removeEventListen(DEFINE_STRING.BOOK.KLING_IMAGE_TO_VIDEO_RETURN)
window.system.removeEventListen(DEFINE_STRING.BOOK.HAILUO_TO_VIDEO_RETURN)
window.system.removeEventListen(DEFINE_STRING.BOOK.COMFYUI_TO_VIDEO_RETURN)
})
//
@ -164,6 +165,10 @@ function handleIpcTaskListChange() {
window.system.setEventListen(DEFINE_STRING.BOOK.HAILUO_TO_VIDEO_RETURN, (value) => {
handleEventReceive(value)
})
window.system.setEventListen(DEFINE_STRING.BOOK.COMFYUI_TO_VIDEO_RETURN, (value) => {
handleEventReceive(value)
})
}
function handleEventReceive(value) {
@ -190,6 +195,10 @@ function handleEventReceive(value) {
let videoMessage = JSON.parse(value.data)
console.log('收到 海螺 video extend 视频处理进度', videoMessage)
handleMessageChange(videoMessage, value.id)
} else if (value.type == ResponseMessageType.COMFYUI_VIDEO) {
let videoMessage = JSON.parse(value.data)
console.log('收到 ComfyUI video 视频处理进度', videoMessage)
handleMessageChange(videoMessage, value.id)
} else if (value.type == ResponseMessageType.VIDEO_SUCESS) {
//
let bookTaskDetail = JSON.parse(value.data)

View File

@ -10,7 +10,7 @@
<template #input="{ submit, deactivate }">
<n-select
size="small"
style="width: 100px"
v-model:value="selectedValue"
:options="show_options"
:placeholder="placeholder"

View File

@ -601,7 +601,9 @@ async function handleImportPrompt() {
{ style: { whiteSpace: 'pre-line' } },
{
default: () =>
t(
`该操作会选择 TXT 文件进行导入提示词,\n\n提示词文件格式要求\n每行一个提示词顺序和当前分镜顺序一致\n如果某个分镜不需要导入提示词可以留空该行\n超出分镜的提示词会被删除不足则只导入文本中有的提示词数据\n\n是否继续`
)
}
),
negativeText: t('取消'),
@ -629,7 +631,6 @@ async function handleImportPrompt() {
let lines = fileContent.split(/\r?\n/).map((line) => line.trim())
//
for (let i = 0; i < lines.length && i < bookStore.selectBookTaskDetail.length; i++) {
const element = lines[i]

View File

@ -41,6 +41,12 @@
<div class="action-buttons">
<n-button type="primary" @click="handleSearch">{{ t('搜索') }}</n-button>
<n-button type="default" @click="handleReset">{{ t('重置') }}</n-button>
<TooltipButton
type="error"
@click="handleDeleteSelect"
:tooltip="t('删除当前搜索出来的所有的预设,若没有搜索条件,会删除全部预设数据!')"
>{{ t('删除选中') }}</TooltipButton
>
</div>
</div>
</template>
@ -51,11 +57,13 @@ import { TimeDelay } from '@/define/Tools/time'
import { t } from '@/i18n'
import { usePresetStore, useSoftwareStore } from '@/renderer/src/stores'
import { Search } from '@vicons/ionicons5'
import DialogTextContent from '@/renderer/src/components/common/DialogTextContent.vue'
const presetStore = usePresetStore()
const softwareStore = useSoftwareStore()
const message = useMessage()
const dialog = useDialog()
//
const handleSearch = async () => {
@ -70,8 +78,6 @@ const handleSearch = async () => {
throw new Error(res.message)
}
presetStore.totalItems = res.data.total ?? 0
console.log('获取预设列表成功', res.data)
presetStore.presetArray = []
await nextTick()
presetStore.presetArray = res.data.presetArray
@ -115,6 +121,69 @@ async function handleReset() {
await handleSearch() //
}
//
async function handleDeleteSelect() {
let da = dialog.create({
title: t('操作提示'),
content: () =>
h(DialogTextContent, {
text: t(
'确定要删除当前搜索出来的所有预设吗?\n若没有任何搜索条件则会删除全部预设数据\n\n此操作不可撤销请谨慎操作'
)
}),
positiveText: t('确认删除'),
negativeText: t('取消'),
onPositiveClick: async () => {
da.destroy()
try {
softwareStore.spin.spinning = true
softwareStore.spin.tip = t('正在批量删除预设数据...')
let condition = {
...presetStore.queryPresetCondition
}
//
condition.pageSize = 1000
//
let res = await window.preset.GetPresetByCondition({
...condition
})
if (res.code != 1) {
throw new Error(res.message)
}
//
console.log(res.data)
let presets = res.data.presetArray ?? []
for (let i = 0; i < presets.length; i++) {
const element = presets[i]
let res = await window.preset.DeletePreset(element.id)
if (res.code != 1) {
message.error(
t('删除预设失败,{error}', {
error: res.message
})
)
return
}
}
message.success(t('批量删除预设成功!'))
//
await handleSearch()
} catch (error) {
message.error(
t('批量删除预设数据失败,{error}', {
error: error.message
})
)
} finally {
softwareStore.spin.spinning = false
}
}
})
}
</script>
<style scoped>

View File

@ -19,9 +19,21 @@
/>
</n-form-item>
<n-form-item :label="t('工作流类型')" path="name">
<n-select
v-model:value="formValue.type"
:placeholder="
t('请选择 {data}', {
data: t('工作流类型')
})
"
:options="getComfyUIWorkflowTypeOptions()"
/>
</n-form-item>
<n-form-item :label="t('工作流文件')" path="jsonFile">
<n-input
v-model:value="formValue.workflowPath"
v-model:value="formValue.workflowFilePath"
:placeholder="
t('请选择 {data}', {
data: t('工作流文件')
@ -44,7 +56,7 @@
<n-button
type="primary"
@click="saveWorkflow"
:disabled="!formValue.name || !formValue.workflowPath"
:disabled="!formValue.name || !formValue.workflowFilePath"
>
{{ isEdit ? t('更新') : t('保存') }}
</n-button>
@ -61,6 +73,7 @@ import { isEmpty } from 'lodash'
import { TimeDelay } from '@/define/Tools/time'
import { ValidateJsonAndParse } from '@/define/Tools/validate'
import { t } from '@/i18n'
import { ComfyUIWorkflowType, getComfyUIWorkflowTypeOptions } from '@/define/enum/comfyuiEnum'
const jsonContent = ref(null)
const formRef = ref(null)
@ -70,7 +83,8 @@ const message = useMessage()
const formValue = ref({
id: null,
name: '',
workflowPath: null
type: ComfyUIWorkflowType.IMAGE,
workflowFilePath: null
})
const rules = {
@ -114,65 +128,44 @@ onMounted(() => {
formValue.value = {
id: props.workflowData.id,
name: props.workflowData.name,
workflowPath: props.workflowData.workflowPath
type: props.workflowData.type,
workflowFilePath: props.workflowData.workflowFilePath
}
}
})
async function saveWorkflow() {
try {
//
const isDuplicate = props.comfyUIWorkFlowSetting.some(
(item) =>
item.name === formValue.value.name && (!isEdit.value || item.id !== formValue.value.id)
)
if (isDuplicate) {
message.error(t('工作流名称已存在,请重新输入'))
return
}
//
let updatedWorkflows = [...props.comfyUIWorkFlowSetting]
let res
if (isEdit.value) {
//
const index = updatedWorkflows.findIndex((item) => item.id === formValue.value.id)
if (index !== -1) {
updatedWorkflows[index] = {
id: formValue.value.id,
const updatedWorkflows = {
name: formValue.value.name,
workflowPath: formValue.value.workflowPath
}
type: formValue.value.type,
workflowFilePath: formValue.value.workflowFilePath
}
res = await window.setting.ModifyWorkflow(formValue.value.id, updatedWorkflows)
} else {
//
updatedWorkflows.push({
const newWorkflow = {
id: crypto.randomUUID(),
name: formValue.value.name,
workflowPath: formValue.value.workflowPath
})
type: formValue.value.type,
workflowFilePath: formValue.value.workflowFilePath
}
res = await window.setting.AddWorkFlow(newWorkflow)
}
//
let res = await window.option.ModifyOptionByKey(
OptionKeyName.SD.ComfyUIWorkFlowSetting,
JSON.stringify(updatedWorkflows),
OptionType.JSON
)
if (res.code == 1) {
message.success(isEdit.value ? t('更新成功') : t('保存成功'))
//
emit('update-workflows', updatedWorkflows)
emit('update-workflows')
//
if (!isEdit.value) {
formValue.value = {
name: '',
workflowPath: null,
workflowFilePath: null,
type: ComfyUIWorkflowType.IMAGE,
id: null
}
jsonContent.value = null
@ -208,9 +201,8 @@ const triggerFileSelect = async () => {
if (res.data == null || isEmpty(res.data)) {
throw new Error(t('未选择任何文件'))
}
formValue.value.workflowPath = res.data
formValue.value.workflowFilePath = res.data
debugger
//
let readTxtRes = await window.system.ReadTextFile(res.data)
@ -244,7 +236,7 @@ const triggerFileSelect = async () => {
* 检查工作流文件是不是正确
*/
async function checkWorkflowFile() {
if (formValue.value.workflowPath == null) {
if (formValue.value.workflowFilePath == null) {
message.error(
t('请选择 {data}', {
data: t('工作流文件')
@ -258,8 +250,10 @@ async function checkWorkflowFile() {
}
console.log(jsonContent.value)
let hasPositivePrompt = false
let hasNegativePrompt = false
let hasFastImage = false
//
let elements = []
@ -276,6 +270,9 @@ async function checkWorkflowFile() {
}
}
console.log(formValue.value)
if (formValue.value.type == ComfyUIWorkflowType.IMAGE) {
for (const element of elements) {
if (element && element.class_type === 'CLIPTextEncode') {
if (element._meta?.title === '正向提示词' || element._meta?.title === 'Positive Prompt') {
@ -286,7 +283,6 @@ async function checkWorkflowFile() {
}
}
}
if (!hasPositivePrompt || !hasNegativePrompt) {
message.error(
t(
@ -294,9 +290,41 @@ async function checkWorkflowFile() {
)
)
return
} else {
message.success(t('工作流文件检查成功通过'))
}
} else if (formValue.value.type == ComfyUIWorkflowType.IMAGE_TO_VIDEO) {
for (const element of elements) {
if (element && element.class_type === 'CLIPTextEncode') {
if (element._meta?.title === '正向提示词' || element._meta?.title === 'Positive Prompt') {
hasPositivePrompt = true
}
if (element._meta?.title === '反向提示词' || element._meta?.title === 'Negative Prompt') {
hasNegativePrompt = true
}
}
if (element && element.class_type === 'LoadImage') {
if (
element._meta?.title === '加载首帧图像' ||
element._meta?.title === 'Load First Frame Image'
) {
hasFastImage = true
}
}
}
if (!hasPositivePrompt || !hasNegativePrompt || !hasFastImage) {
message.error(
t(
'工作流文件缺少正向提示词、反向提示词或加载首帧图像模块,请检查工作流文件,把对应的文本编码模块的标题改为正向提示词和反向提示词,把加载图像模块的标题改为加载首帧图像!!'
)
)
return
}
} else {
message.error(t('未知的工作流类型,请检查工作流类型'))
return
}
message.success(t('工作流文件检查成功通过'))
}
</script>

View File

@ -1,8 +1,16 @@
<template>
<div class="comfy-ui-setting">
<div class="form-section">
<h3 class="section-title">{{ t('ComfyUI 基础设置') }}</h3>
<n-form inline :model="comfyUISimpleSetting" class="inline-form">
<n-card :title="t('ComfyUI 设置')" class="setting-card">
<n-divider title-placement="left"> {{ t('通用设置') }} </n-divider>
<n-form
inline
:model="comfyUISimpleSetting"
class="inline-form"
:style="{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'flex-start'
}"
>
<n-form-item :label="t('请求地址')" path="requestUrl">
<n-input
v-model:value="comfyUISimpleSetting.requestUrl"
@ -13,16 +21,37 @@
"
/>
</n-form-item>
<n-form-item :label="t('使用工作流')" path="selectedWorkflow">
<n-form-item :label="t('生图工作流')" path="selectedWorkflow">
<n-select
:placeholder="
t('请选择 {data}', {
data: t('使用工作流')
data: t('生图工作流')
})
"
v-model:value="comfyUISimpleSetting.selectedWorkflow"
:options="
comfyUIWorkFlowSetting.map((workflow) => ({
comfyUIWorkFlowSetting
.filter((w) => isEmpty(w.type) || w.type == ComfyUIWorkflowType.IMAGE)
.map((workflow) => ({
label: workflow.name,
value: workflow.id
}))
"
style="width: 200px"
/>
</n-form-item>
<n-form-item :label="t('视频工作流')" path="imageToVideoSelectWorkflow">
<n-select
:placeholder="
t('请选择 {data}', {
data: t('视频工作流')
})
"
v-model:value="comfyUISimpleSetting.imageToVideoSelectWorkflow"
:options="
comfyUIWorkFlowSetting
.filter((w) => w.type == ComfyUIWorkflowType.IMAGE_TO_VIDEO)
.map((workflow) => ({
label: workflow.name,
value: workflow.id
}))
@ -47,26 +76,82 @@
</n-button>
</n-form-item>
</n-form>
<n-card
class="notice-card"
embedded
style="margin: 16px 0; border: 2px solid #ff4d4f; background: #fff1f0"
>
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="font-size: 18px; color: #d4380d; font-weight: bold">
{{ t('⚠️ ComfyUI 工作流配置请严格参考文档,否则无法正常生成!') }}
</div>
<div style="color: red">
<p>{{ t('注意事项') }}</p>
<p
v-html="
t('1. Comfy UI的工作流中正向提示词和反向提示必须为 <strong>Clip文本编码</strong> 节点')
"
></p>
<p v-html="t('2. 标题必须对应 <strong>正向提示词和反向提示词</strong>')"></p>
<p
v-html="
t(
'3 图像输出节点必须是 <strong>保存图像</strong> 节点,<strong>采样器只支持简单 K采样器和K采样器高级</strong>'
)
"
></p>
<n-button type="error" size="large" @click="openComfyUIDoc" style="margin-left: 24px">
{{ t('查看文档') }}
</n-button>
</div>
<div class="action-bar">
</n-card>
<n-divider title-placement="left"> {{ t('工作流设置') }} </n-divider>
<n-form
inline
:model="queryForm"
class="inline-form"
@submit.prevent="handleQuery"
:style="{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'flex-start'
}"
>
<n-form-item :label="t('工作流名称')" path="name">
<n-input
v-model:value="queryForm.name"
:placeholder="
t('请输入 {data}', {
data: t('工作流名称')
})
"
clearable
/>
</n-form-item>
<n-form-item :label="t('工作流类型')" path="type">
<n-select
v-model:value="queryForm.type"
:options="workflowTypeOptions"
:placeholder="
t('请选择 {data}', {
data: t('工作流类型')
})
"
clearable
style="width: 200px"
/>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleQuery">{{ t('查询') }}</n-button>
</n-form-item>
<n-form-item>
<n-button tertiary @click="handleResetQuery">{{ t('重置') }}</n-button>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleAdd">{{ t('添加') }}</n-button>
</div>
</n-form-item>
<n-form-item>
<TooltipButton
type="error"
:disabled="checkedRowKeys.length === 0"
@click="handleBatchDelete"
tooltip="123"
>
{{
t('批量删除({count}', {
count: checkedRowKeys.length > 0 ? checkedRowKeys.length : 0
})
}}
</TooltipButton>
</n-form-item>
</n-form>
<div ref="tableContainer" class="table-container">
<n-data-table
:columns="columns"
@ -74,13 +159,17 @@
:bordered="true"
:single-line="false"
:max-height="tableHeight"
:row-key="rowKey"
v-model:checked-row-keys="checkedRowKeys"
:pagination="pagination"
@update:checked-row-keys="handleCheck"
/>
</div>
</div>
</n-card>
</template>
<script setup>
import { ref, h, onMounted, onUnmounted, nextTick, toRaw } from 'vue'
import { ref, h, onMounted, onUnmounted, nextTick, toRaw, computed } from 'vue'
import {
NDataTable,
NButton,
@ -96,15 +185,70 @@ import AddComfyUIWorkflow from './ComfyUIAddWorkflow.vue'
import { OptionKeyName, OptionType } from '@/define/enum/option'
import { optionSerialization } from '@/main/service/option/optionSerialization'
import { t } from '@/i18n'
import { isEmpty } from 'lodash'
import {
ComfyUIWorkflowType,
getComfyUIWorkflowTypeLabel,
getComfyUIWorkflowTypeOptions
} from '@/define/enum/comfyuiEnum'
import { SoftwareData } from '@/define/data/softwareData'
//
const comfyUISimpleSetting = ref({
requestUrl: '',
selectedWorkflow: '',
imageToVideoSelectWorkflow: '',
negativePrompt: ''
})
const comfyUIWorkFlowSetting = ref([])
const totalCount = ref(0) //
const queryForm = ref({
name: '',
type: null
})
//
const checkedRowKeys = ref([])
const rowKey = (row) => row.id
//
const currentPage = ref(1)
const pageSize = ref(100)
const pageSizes = [10, 20, 30, 50, 100]
const workflowTypeOptions = computed(() =>
getComfyUIWorkflowTypeOptions().map((option) => ({
label: t(option.label),
value: option.value
}))
)
//
const pagination = computed(() => ({
page: currentPage.value,
pageSize: pageSize.value,
itemCount: totalCount.value,
pageSizes: pageSizes,
showSizePicker: true,
showQuickJumper: false,
prefix: (info) => {
return t('共 {count} 条', { itemCount: info.itemCount })
},
onUpdatePage: (page) => {
currentPage.value = page
loadWorkflowData() //
},
onUpdatePageSize: (size) => {
pageSize.value = size
currentPage.value = 1 //
loadWorkflowData() //
}
}))
//
const handleCheck = (keys) => {
checkedRowKeys.value = keys
}
const dialog = useDialog()
let message = useMessage()
@ -140,19 +284,40 @@ async function initializeSimpleSetting() {
//
async function initializeWorkflowSetting() {
await loadWorkflowData()
}
//
async function loadWorkflowData() {
try {
const workflowSettingRes = await window.option.GetOptionByKey(
OptionKeyName.SD.ComfyUIWorkFlowSetting
)
if (workflowSettingRes.code != 1) {
const condition = {
page: currentPage.value,
pageSize: pageSize.value
}
if (!isEmpty(queryForm.value.name)) {
condition.name = queryForm.value.name.trim()
}
if (!isEmpty(queryForm.value.type)) {
condition.type = queryForm.value.type
}
let res = await window.setting.GetWorkFlowByCondition(condition)
console.log('loadWorkflowData res', res)
if (res.code !== 1) {
message.error(t('获取ComfyUI工作流设置失败'))
return
}
debugger
comfyUIWorkFlowSetting.value = optionSerialization(workflowSettingRes.data)
//
comfyUIWorkFlowSetting.value = res.data.workflowArray || []
totalCount.value = res.data.total || 0
} catch (error) {
message.error(
t('初始化设置失败,{error}', {
t('加载工作流数据失败,{error}', {
error: error.message
})
)
@ -160,6 +325,20 @@ async function initializeWorkflowSetting() {
}
}
async function handleQuery() {
currentPage.value = 1
await loadWorkflowData()
}
async function handleResetQuery() {
queryForm.value = {
name: '',
type: null
}
currentPage.value = 1
await loadWorkflowData()
}
//
async function initializeData() {
try {
@ -224,6 +403,11 @@ async function SaveComfyUISimpleSetting() {
}
}
//
function openComfyUIDoc() {
window.system.OpenUrl(SoftwareData.systemInfo.comfyUIWorkflowDoc)
}
//
const tableContainer = ref(null)
const tableHeight = ref(300) //
@ -249,13 +433,24 @@ const updateTableHeight = () => {
// Table columns
const columns = [
{
type: 'selection'
},
{
title: t('工作流名称'),
key: 'name'
},
{
title: t('工作流类型'),
key: 'type',
width: 120,
render(row) {
return t(getComfyUIWorkflowTypeLabel(row.type))
}
},
{
title: t('工作流文件'),
key: 'workflowPath'
key: 'workflowFilePath'
},
{
title: t('操作'),
@ -308,7 +503,7 @@ const handleAdd = () => {
h(AddComfyUIWorkflow, {
comfyUIWorkFlowSetting: toRaw(comfyUIWorkFlowSetting.value),
onUpdateWorkflows: async () => {
await initializeWorkflowSetting()
await loadWorkflowData() //
},
onDialogClose: () => da?.destroy()
})
@ -326,8 +521,8 @@ const handleEdit = (row) => {
h(AddComfyUIWorkflow, {
workflowData: row,
comfyUIWorkFlowSetting: toRaw(comfyUIWorkFlowSetting.value),
onUpdateWorkflows: (updatedWorkflows) => {
comfyUIWorkFlowSetting.value = updatedWorkflows
onUpdateWorkflows: async () => {
await loadWorkflowData() //
},
onDialogClose: () => da?.destroy()
})
@ -345,53 +540,93 @@ const handleRemove = (row) => {
negativeText: t('取消'),
onPositiveClick: async () => {
try {
//
const index = comfyUIWorkFlowSetting.value.findIndex((item) => item.id === row.id)
if (index == -1) {
message.error(t('删除失败: 未找到要删除的工作流'))
//
const res = await window.setting.DeleteWorkflow(row.id)
if (res.code !== 1) {
message.error(
t('删除失败,{error}', {
error: res.message
})
)
return
}
comfyUIWorkFlowSetting.value.splice(index, 1)
//
if (comfyUISimpleSetting.value.selectedWorkflow === row.id) {
comfyUISimpleSetting.value.selectedWorkflow = ''
}
//
let res = await window.option.ModifyOptionByKey(
OptionKeyName.ComfyUI_WorkFlowSetting,
JSON.stringify(comfyUIWorkFlowSetting.value),
//
await window.option.ModifyOptionByKey(
OptionKeyName.SD.ComfyUISimpleSetting,
JSON.stringify(comfyUISimpleSetting.value),
OptionType.JSON
)
}
if (res.code == 0) {
message.success(t('删除成功'))
//
await loadWorkflowData()
} catch (error) {
message.error(
t('删除失败,{error}', {
error: error.message
})
)
}
}
})
}
//
const handleBatchDelete = () => {
if (checkedRowKeys.value.length === 0) {
message.warning(t('请先选择要删除的工作流'))
return
}
dialog.warning({
title: t('操作确认'),
content: t('确定要删除选中的 {count} 个工作流吗?此操作不可撤销。是否继续?', {
count: checkedRowKeys.value.length
}),
positiveText: t('确定'),
negativeText: t('取消'),
onPositiveClick: async () => {
try {
//
const res = await window.setting.DeleteWorkflowByIds([...checkedRowKeys.value])
if (res.code !== 1) {
message.error(
t('批量删除工作流失败:{error}', {
error: res.message
})
)
return
}
res = await window.option.ModifyOptionByKey(
OptionKeyName.ComfyUI_SimpleSetting,
//
if (checkedRowKeys.value.includes(comfyUISimpleSetting.value.selectedWorkflow)) {
comfyUISimpleSetting.value.selectedWorkflow = ''
//
await window.option.ModifyOptionByKey(
OptionKeyName.SD.ComfyUISimpleSetting,
JSON.stringify(comfyUISimpleSetting.value),
OptionType.JSON
)
if (res.code == 1) {
message.success(t('删除成功'))
} else {
message.error(
t('删除失败,{error}', {
error: res.message
})
)
}
//
checkedRowKeys.value = []
message.success(t('批量删除工作流成功'))
//
await loadWorkflowData()
} catch (error) {
message.error(
t('删除失败,{error}', {
t('批量删除工作流失败:{error}', {
error: error.message
})
)
@ -415,4 +650,10 @@ const handleRemove = (row) => {
.table-container {
width: 100%;
}
.notice-card {
margin: 16px 0;
border: 2px solid #ff4d4f;
background: #fff1f0;
}
</style>

View File

@ -43,7 +43,7 @@
v-model:value="keyFrameData.keyFrameTime"
style="width: 100px"
/>
<n-button type="info" @click="SaveKeyFrameSetting">保存</n-button>
<n-button type="primary" @click="SaveKeyFrameSetting">保存</n-button>
</n-space>
<n-card style="margin-top: 10px" :title="t('上下关键帧设置')">
<n-space align="center" :size="12">

View File

@ -1,6 +1,6 @@
<template>
<!-- 通用设置部分 -->
<n-card title="Midjourney 设置" class="setting-card">
<n-card :title='t("Midjourney 设置")' class="setting-card">
<n-divider title-placement="left"> {{ t('通用设置') }} </n-divider>
<!-- 通用设置部分表单 -->
<n-form

View File

@ -174,7 +174,7 @@
</n-input>
<n-image
v-if="!isEmpty(getOptionValue(option.key))"
:src="getOptionValue(option.key)"
:src="getImageSrc(option.key)"
:height="option.previewHeight || 60"
:fallback-src="'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2Y1ZjVmNSIvPgogIDx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSIjOTk5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iMC4zZW0iPuWbvueJh+WKoOi9veWksei0pTwvdGV4dD4KPC9zdmc+'"
object-fit="contain"
@ -200,7 +200,8 @@ import {
NIcon,
NTooltip,
NAlert,
NImage} from 'naive-ui'
NImage
} from 'naive-ui'
import { HelpCircleOutline } from '@vicons/ionicons5'
import { computed } from 'vue'
import { t } from '@/i18n'
@ -292,7 +293,7 @@ const validationErrors = computed(() => {
//
switch (option.type) {
case 'select':
if (!option.options || !Array.isArray(option.options) || option.options.length === 0) {
if (!option.options || !Array.isArray(option.options)) {
errors.push(`选项 ${index + 1} (${option.key}): select 类型需要 options 数组且不能为空`)
} else {
// options
@ -363,6 +364,14 @@ const validationErrors = computed(() => {
//
const hasValidationErrors = computed(() => validationErrors.value.length > 0)
// URLcomputed
const getImageSrc = computed(() => (key) => {
const imageUrl = props.value[key]
if (!imageUrl) return ''
const separator = imageUrl.includes('?') ? '&' : '?'
return `${imageUrl}${separator}t=${Date.now()}`
})
//
function getOptionValue(key) {
return props.value[key]

View File

@ -0,0 +1,29 @@
<template>
<div :style="computedStyle">
{{ text }}
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
text: {
type: String,
required: true
},
whiteSpace: {
type: String,
default: 'pre-line'
},
additionalStyles: {
type: Object,
default: () => ({})
}
})
const computedStyle = computed(() => ({
whiteSpace: props.whiteSpace,
...props.additionalStyles
}))
</script>

View File

@ -0,0 +1,32 @@
import { WorkflowModel } from "@/define/model/workflow"
import { t } from "@/i18n"
export function useVideo() {
async function getComfyuiWorkflow(condition: WorkflowModel.QueryWorkflowCondition) {
try {
let res = await window.setting.GetWorkFlowByCondition(condition);
if (res.code != 1) {
throw new Error(t('获取ComfyUI工作流失败{error}', { error: res.message }))
}
if (res.data.workflowArray == null || res.data.workflowArray.length === 0) {
return [];
}
for (let i = 0; i < res.data.workflowArray.length; i++) {
const element = res.data.workflowArray[i];
element.label = element.name;
element.value = element.id;
}
return res.data.workflowArray;
} catch (error) {
throw error;
}
}
return {
getComfyuiWorkflow
}
}

View File

@ -92,10 +92,12 @@
</n-grid-item>
<n-grid-item>
<n-form-item path="taskErrorMessage">
<n-button type="info" @click="ResetQuery">{{ t('重置') }}</n-button>
<n-button style="margin-left: 10px" type="info" @click="QueryByByCondition">{{
<n-space>
<n-button style="margin-left: 10px" type="primary" @click="QueryByByCondition">{{
t('查询')
}}</n-button>
<n-button type="default" @click="ResetQuery">{{ t('重置') }}</n-button>
</n-space>
</n-form-item>
</n-grid-item>
</n-grid>

164
updateInfo.json Normal file
View File

@ -0,0 +1,164 @@
{
"latestVersion": "v4.0.4",
"updateDate": "2025-11-05",
"updateInfo": [
{
"version": "v4.0.4",
"updateDate": "2025-11-05",
"status": "unreleased",
"changes": [
{
"type": "add",
"description": "新增预设库的批量删除"
},
{
"type": "add",
"description": "添加两个高图文一致性推理预设"
},
{
"type": "add",
"description": "新增 ComfyUI 图转视频功能(之前的工作流需要重新配置)"
},
{
"type": "improvement",
"description": "优化 ComfyUI 设置"
},
{
"type": "add",
"description": "新增导入图转视频提示词"
},
{
"type": "add",
"description": "新增同步出图提示词到图转视频提示词"
}
]
},
{
"version": "v4.0.3",
"updateDate": "2025-09-26",
"status": "released",
"changes": [
{
"type": "add",
"description": "新增 海螺生成视频(文生视频,图转视频,首尾帧视频)"
},
{
"type": "bugfix",
"description": "修复MJ出图的部分问题"
},
{
"type": "improvement",
"description": "优化原创的加载速度,分批次渲染,加快界面显示速度"
}
]
},
{
"version": "v4.0.2",
"updateDate": "2025-09-23",
"status": "released",
"changes": [
{
"type": "add",
"description": "新增原创导入提示词"
},
{
"type": "add",
"description": "新增 MJ Video 批量设置基础设置"
},
{
"type": "add",
"description": "新增原创设置通用前缀和通用后缀"
},
{
"type": "add",
"description": "添加可灵的图转视频和视频延长"
}
]
},
{
"version": "v4.0.1",
"updateDate": "2025-09-21",
"status": "released",
"changes": [
{
"type": "bugfix",
"description": "修改场景推理,导入到场景预设时原创界面的自动更新分组错误"
},
{
"type": "add",
"description": "文案处理可单独设置API、密钥、推理设置没有设置就默认使用推理设置"
},
{
"type": "improvement",
"description": "修改MJ出图的代理模式添加账号修改账号出图"
},
{
"type": "improvement",
"description": "优化剪映关键帧设置UI界面"
},
{
"type": "bugfix",
"description": "修复文案处理的单个清空和批量清空"
},
{
"type": "remove",
"description": "删除 MJ Video Extend 的尾帧链接"
}
]
},
{
"version": "v4.0.0",
"updateDate": "2025-09-19",
"status": "released",
"changes": [
{
"type": "improvement",
"description": "软件全新重构升级所有UI"
},
{
"type": "add",
"description": "新增自定义主题颜色,可手动选择不同的颜色"
},
{
"type": "add",
"description": "新增全新的预设库,集中操作"
},
{
"type": "add",
"description": "新增/完善图转视频完美适配MJ Video 的所有的参数"
},
{
"type": "add",
"description": "全新的文案处理界面全新的操作逻辑和UI界面"
},
{
"type": "improvement",
"description": "全新的原创界面,添加手动分组和文案分组,更加的优化的预设使用"
}
]
},
{
"version": "v3.4.3",
"updateDate": "2025-08-16",
"status": "released",
"changes": [
{
"type": "improvement",
"description": "导出剪映草稿,字幕自动换行"
},
{
"type": "bugfix",
"description": "修复一键导出草稿保存数据"
},
{
"type": "bugfix",
"description": "修复 一拆四 无效"
},
{
"type": "improvement",
"description": "适配 暗主题下的导出草稿界面"
}
]
}
]
}