koishi-plugin-p-draw 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +297 -11
- package/package.json +1 -1
- package/readme.md +14 -3
package/index.js
CHANGED
|
@@ -28,7 +28,11 @@ exports.usage = `
|
|
|
28
28
|
- **联网搜索:** 描述中带 \`联网\` / \`搜索\` / \`查一下\` 等词时,会先联网搜索补充角色设定(需配置 Tavily Key)。
|
|
29
29
|
- **画师组:** \`创建画师组 名称=tags\` \`切换画师组 名称\` \`查看画师组\` \`删除画师组 名称\`
|
|
30
30
|
- **固定角色:** \`添加角色 名称=tags\`
|
|
31
|
-
- **以图生图:** \`p-draw i2i
|
|
31
|
+
- **以图生图:** \`p-draw i2i <描述>\`,并在同一条消息里附一张原图(文件/截图/链接均可)。发送后会询问处理模式:
|
|
32
|
+
- **① 换风格(漫画化)**:保留原图构图,把图片转成二次元画风(用 ControlNet 锁结构)
|
|
33
|
+
- **② 换装换姿势**:保留角色长相,重新设计服装/姿势/场景(用 IP-Adapter 保脸)
|
|
34
|
+
- **③ 取消**:不生成
|
|
35
|
+
- 可加 \`--denoise 0.6\` 单独调整强度;未装 ControlNet/IPAdapter 时自动回退普通 img2img。
|
|
32
36
|
`;
|
|
33
37
|
|
|
34
38
|
const zhCN = {
|
|
@@ -74,7 +78,19 @@ const zhCN = {
|
|
|
74
78
|
multiPrice: { $description: '多人指令(p-draw 多人)单张消耗的 P 点' },
|
|
75
79
|
couponPrice: { $description: '提示词优化券单价(P 点/张,购买询问时显示)' },
|
|
76
80
|
couponAskTimeout: { $description: '提示词优化券确认等待时间(秒)' },
|
|
77
|
-
img2imgDenoise: { $description: '
|
|
81
|
+
img2imgDenoise: { $description: '普通以图生图(p-draw i2i)的去噪强度,越小越接近原图(建议 0.4-0.7)' },
|
|
82
|
+
i2iMode: { $description: 'i2i 模式选择方式:ask=每次询问 / style=直接换风格(漫画化)/ ootd=直接换装换姿势 / plain=普通 img2img' },
|
|
83
|
+
i2iAskTimeout: { $description: 'i2i 模式询问等待时间(秒)' },
|
|
84
|
+
i2iStyleDenoise: { $description: '换风格模式(漫画化)的去噪强度,越大风格变化越彻底(建议 0.7-0.85)' },
|
|
85
|
+
i2iOotdDenoise: { $description: '换装换姿势模式(保留角色)的去噪强度(建议 0.5-0.6)' },
|
|
86
|
+
i2iControlNetStrength: { $description: '换风格模式的 ControlNet 强度,越大构图锁得越死(建议 0.5-0.8)' },
|
|
87
|
+
i2iIPAdapterPath: { $description: 'Anima IP-Adapter 模型文件路径(换装换姿势模式保脸用;需安装 comfyui-anima-ipadapter 节点并把模型路径填到这里)' },
|
|
88
|
+
i2iIPAdapterWeight: { $description: '换装换姿势模式的 IP-Adapter 权重,越大角色特征保留越强(建议 0.6-1.0)' },
|
|
89
|
+
controlNetModel: { $description: 'ControlNet 模型文件名(留空自动检测 Qwen/Anima 系 ControlNet;需放到 ComfyUI/models/controlnet)' },
|
|
90
|
+
taggerEnabled: { $description: 'i2i 前自动识图(需 ComfyUI 安装 WD14 Tagger 节点与模型;识别出的标签会注入提示词优化)' },
|
|
91
|
+
taggerModel: { $description: '识图模型名(WD14 Tagger 节点里可选模型)' },
|
|
92
|
+
taggerThreshold: { $description: '识图标签置信度阈值' },
|
|
93
|
+
taggerCharacterThreshold: { $description: '识图角色标签置信度阈值' },
|
|
78
94
|
adminUsers: { $description: '免 P 点管理员用户 ID 列表' },
|
|
79
95
|
outputLogs: { $description: '是否在控制台输出详细日志' },
|
|
80
96
|
multiVerifyEnabled: { $description: '多人图生成后启用视觉校验(需配置下方视觉模型)' },
|
|
@@ -94,7 +110,7 @@ const zhCN = {
|
|
|
94
110
|
description: '连接本地 ComfyUI 生图,消耗 P 点',
|
|
95
111
|
messages: {
|
|
96
112
|
'not-permitted': 'ComfyUI 助手已关闭,或当前用户没有使用权限。',
|
|
97
|
-
usage: 'p-draw 帮助(本指令名可自行更换,如 /anm):\n\n【生成】\n p-draw <描述>\n 例:p-draw 一个女孩,白色裙子,立绘,简单背景\n 可加 --seed 数字 固定种子(单张 / 批量 / 连续图均支持)\n\n【多人生成】\n p-draw 多人 <描述>(2-4 人画面)\n 例:p-draw 多人 左边若叶睦抱着吉他,右边千早爱音牵着她的手\n\n【连续图】\n p-draw 连续 <角色>:<阶段1> → <阶段2> → ...\n 例:p-draw 连续 少女:清纯校服 → 换上晚礼服 → 华丽登场\n 全阶段共用同一 seed,角色外观尽量一致;可加 --seed 数字 固定种子\n 阶段分隔:→ / -> / |\n\n【批量张数】\n 描述后加 x3 / ×3 / 3张 / 三张 / --数量 3,例:p-draw 一个女孩 x3\n 多张按 张数×单价 一次性扣 P 点,余额不足则不生成\n\n【尺寸】\n 竖图 / 横图 / 方图 / 长竖图 / 宽屏,或 1024x1536:描述 / --尺寸 1216x832\n 例:p-draw 竖图:狐莉站在梨花树下\n\n【原样模式】\n p-draw 无优化 masterpiece, best quality, 1girl, solo\n\n【提示词优化】\n 配置 LLM(llmBaseUrl/llmModel)后:\n 全局优化开启 → 每次自动把中文描述转成 Danbooru tags\n 全局优化关闭 → 生图时询问是否使用「提示词优化券」(p-shop 购买,每张图扣 1 张):\n 有券 → 确认后消耗券并优化,拒绝则取消本次生图\n 没券 → 询问是否购买(显示价格),确认后购买并消耗、优化生图;\n 拒绝一次会再次警告,再拒绝则直接生图(不优化);\n P 点不足买不起券时,会询问是否仍然生图\n 无优化 <tags> 原样生图,不耗券\n 连续图逐阶段强制优化;多人指令依赖 LLM 规划(未配置会提示)\n\n【画师组】\n 创建画师组 名称=tags / 追加画师组 名称=tags / 切换画师组 名称 / 查看画师组 / 删除画师组 名称\n\n【固定角色】\n 添加角色 名称=tags\n\n【以图生图】\n p-draw i2i <描述>,并在同一条消息里附一张原图(文件/截图/链接均可)\n 例:p-draw i2i 换成晚礼服,背景换成舞台灯光\n
|
|
113
|
+
usage: 'p-draw 帮助(本指令名可自行更换,如 /anm):\n\n【生成】\n p-draw <描述>\n 例:p-draw 一个女孩,白色裙子,立绘,简单背景\n 可加 --seed 数字 固定种子(单张 / 批量 / 连续图均支持)\n\n【多人生成】\n p-draw 多人 <描述>(2-4 人画面)\n 例:p-draw 多人 左边若叶睦抱着吉他,右边千早爱音牵着她的手\n\n【连续图】\n p-draw 连续 <角色>:<阶段1> → <阶段2> → ...\n 例:p-draw 连续 少女:清纯校服 → 换上晚礼服 → 华丽登场\n 全阶段共用同一 seed,角色外观尽量一致;可加 --seed 数字 固定种子\n 阶段分隔:→ / -> / |\n\n【批量张数】\n 描述后加 x3 / ×3 / 3张 / 三张 / --数量 3,例:p-draw 一个女孩 x3\n 多张按 张数×单价 一次性扣 P 点,余额不足则不生成\n\n【尺寸】\n 竖图 / 横图 / 方图 / 长竖图 / 宽屏,或 1024x1536:描述 / --尺寸 1216x832\n 例:p-draw 竖图:狐莉站在梨花树下\n\n【原样模式】\n p-draw 无优化 masterpiece, best quality, 1girl, solo\n\n【提示词优化】\n 配置 LLM(llmBaseUrl/llmModel)后:\n 全局优化开启 → 每次自动把中文描述转成 Danbooru tags\n 全局优化关闭 → 生图时询问是否使用「提示词优化券」(p-shop 购买,每张图扣 1 张):\n 有券 → 确认后消耗券并优化,拒绝则取消本次生图\n 没券 → 询问是否购买(显示价格),确认后购买并消耗、优化生图;\n 拒绝一次会再次警告,再拒绝则直接生图(不优化);\n P 点不足买不起券时,会询问是否仍然生图\n 无优化 <tags> 原样生图,不耗券\n 连续图逐阶段强制优化;多人指令依赖 LLM 规划(未配置会提示)\n\n【画师组】\n 创建画师组 名称=tags / 追加画师组 名称=tags / 切换画师组 名称 / 查看画师组 / 删除画师组 名称\n\n【固定角色】\n 添加角色 名称=tags\n\n【以图生图】\n p-draw i2i <描述>,并在同一条消息里附一张原图(文件/截图/链接均可)\n 例:p-draw i2i 换成晚礼服,背景换成舞台灯光\n 发送后会询问处理模式(可配置 i2iMode 固定模式跳过询问):\n ① 换风格(漫画化):保留原图构图,转成二次元画风(需 ControlNet)\n ② 换装换姿势:保留角色长相,重新设计服装/姿势/场景(需 Anima IP-Adapter)\n ③ 取消:不生成\n 可加 --denoise 0.6 单独调整强度;未装 ControlNet/IPAdapter 时自动回退普通 img2img\n\n【模型】\n p-draw 模型(查看当前与可用模型) / p-draw 模型 名称(切换,支持模糊匹配)/ p-draw 模型 默认(重置)\n 例:p-draw 模型 anima-aesthetic\n\n【状态】\n p-draw 状态(查看 ComfyUI 连接状态与模型可用性)',
|
|
98
114
|
'account-notExists': '君现在还没有 p 点,请先签到哦',
|
|
99
115
|
'no-enough-p': '君的 p 点不够 {0}p 哦,先去签个到吧qwq',
|
|
100
116
|
'no-prompt': '请提供画面描述,例如:p-draw 一个女孩,白色裙子',
|
|
@@ -151,6 +167,13 @@ const zhCN = {
|
|
|
151
167
|
'i2i-no-image': 'i2i(以图生图)需要附一张原图。用法:p-draw i2i <描述>,并在同一条消息里带上图片。',
|
|
152
168
|
'i2i-upload-fail': '原图上传 ComfyUI 失败:{0}',
|
|
153
169
|
'i2i-no-custom-workflow': 'i2i(以图生图)暂不支持自定义工作流(customWorkflowEnabled),请关闭后再试。',
|
|
170
|
+
'i2i-mode-ask': '请选择 i2i 处理模式:\n① 换风格(漫画化)——保留原图构图,转成二次元画风\n② 换装换姿势——保留角色长相,重新设计服装/姿势/场景\n③ 取消——不生成\n回复 1 / 2 / 3 或对应名称即可。',
|
|
171
|
+
'i2i-mode-invalid': '没有理解你的选择。请回复 ①换风格(漫画化) / ②换装换姿势 / ③取消。',
|
|
172
|
+
'i2i-mode-cancelled': '已取消本次 i2i 生图。',
|
|
173
|
+
'i2i-mode-style': '已选择【换风格(漫画化)】:保留原图构图,转成二次元画风。',
|
|
174
|
+
'i2i-mode-ootd': '已选择【换装换姿势】:保留角色长相,重新设计服装/姿势/场景。',
|
|
175
|
+
'i2i-no-controlnet': '未检测到可用的 ControlNet 模型,本次「换风格」将使用普通 img2img,构图保留效果会弱一些。',
|
|
176
|
+
'i2i-no-ipadapter': '未检测到可用的 Anima IP-Adapter,本次「换装换姿势」将使用普通 img2img,角色保留效果会弱一些。',
|
|
154
177
|
'multi-no-llm': '多人指令需要 LLM 规划,但当前未配置 llmBaseUrl / llmModel。请管理员在配置中填写后使用。',
|
|
155
178
|
},
|
|
156
179
|
},
|
|
@@ -210,7 +233,19 @@ exports.Config = Schema.object({
|
|
|
210
233
|
multiPrice: Schema.number().default(900).description('多人指令(p-draw 多人)单张消耗的 P 点'),
|
|
211
234
|
couponPrice: Schema.number().default(3000).description('提示词优化券单价(P 点/张,购买询问时显示;可自动读取 data/p-shop.json 里的价格覆盖)'),
|
|
212
235
|
couponAskTimeout: Schema.number().default(60).description('提示词优化券确认等待时间(秒)'),
|
|
213
|
-
img2imgDenoise: Schema.number().default(0.55).description('
|
|
236
|
+
img2imgDenoise: Schema.number().default(0.55).description('普通以图生图(p-draw i2i)的去噪强度,越小越接近原图(建议 0.4-0.7)'),
|
|
237
|
+
i2iMode: Schema.string().default('ask').description('i2i 模式选择方式:ask=每次询问 / style=直接换风格(漫画化)/ ootd=直接换装换姿势 / plain=普通 img2img'),
|
|
238
|
+
i2iAskTimeout: Schema.number().default(60).description('i2i 模式询问等待时间(秒)'),
|
|
239
|
+
i2iStyleDenoise: Schema.number().default(0.75).description('换风格模式(漫画化)的去噪强度,越大风格变化越彻底(建议 0.7-0.85)'),
|
|
240
|
+
i2iOotdDenoise: Schema.number().default(0.55).description('换装换姿势模式(保留角色)的去噪强度(建议 0.5-0.6)'),
|
|
241
|
+
i2iControlNetStrength: Schema.number().default(0.7).description('换风格模式的 ControlNet 强度,越大构图锁得越死(建议 0.5-0.8)'),
|
|
242
|
+
i2iIPAdapterPath: Schema.string().default('').description('Anima IP-Adapter 模型文件路径(换装换姿势模式保脸用;需安装 comfyui-anima-ipadapter 节点并把模型路径填到这里)'),
|
|
243
|
+
i2iIPAdapterWeight: Schema.number().default(0.8).description('换装换姿势模式的 IP-Adapter 权重,越大角色特征保留越强(建议 0.6-1.0)'),
|
|
244
|
+
controlNetModel: Schema.string().default('').description('ControlNet 模型文件名(留空自动检测 Qwen/Anima 系 ControlNet;需放到 ComfyUI/models/controlnet)'),
|
|
245
|
+
taggerEnabled: Schema.boolean().default(false).description('i2i 前自动识图(需 ComfyUI 安装 WD14 Tagger 节点与模型;识别出的标签会注入提示词优化)'),
|
|
246
|
+
taggerModel: Schema.string().default('wd-v1-4-convnext-tagger-v2').description('识图模型名(WD14 Tagger 节点里可选模型)'),
|
|
247
|
+
taggerThreshold: Schema.number().default(0.35).description('识图标签置信度阈值'),
|
|
248
|
+
taggerCharacterThreshold: Schema.number().default(0.85).description('识图角色标签置信度阈值'),
|
|
214
249
|
adminUsers: Schema.array(Schema.string()).default([]).description('免 P 点管理员用户 ID 列表'),
|
|
215
250
|
outputLogs: Schema.boolean().default(true).description('是否在控制台输出详细日志'),
|
|
216
251
|
|
|
@@ -381,6 +416,18 @@ function parseSeed(text) {
|
|
|
381
416
|
return { seed, prompt: cleaned }
|
|
382
417
|
}
|
|
383
418
|
|
|
419
|
+
// 去噪强度解析:--denoise 0.3 / --denoise=0.6 / --去噪 0.4,并从提示词里剥离
|
|
420
|
+
function parseDenoise(text) {
|
|
421
|
+
const prompt = String(text || '').trim()
|
|
422
|
+
const m = prompt.match(/(?<!\S)--(?:denoise|去噪)\s*(?:=|=|:|:)?\s*(\d+(?:\.\d+)?)/i)
|
|
423
|
+
if (!m) return { denoise: null, prompt }
|
|
424
|
+
const denoise = Math.min(1, Math.max(0, parseFloat(m[1])))
|
|
425
|
+
const cleaned = (prompt.slice(0, m.index) + ' ' + prompt.slice(m.index + m[0].length)).trim()
|
|
426
|
+
.replace(/^[\s,,;;::]+|[\s,,;;::]+$/g, '')
|
|
427
|
+
.replace(/\s+/g, ' ')
|
|
428
|
+
return { denoise, prompt: cleaned }
|
|
429
|
+
}
|
|
430
|
+
|
|
384
431
|
// ------------------------------------------------------------------
|
|
385
432
|
// 提示词辅助(移植自 anima prompt_presets)
|
|
386
433
|
// ------------------------------------------------------------------
|
|
@@ -519,6 +566,109 @@ function animaI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfg
|
|
|
519
566
|
}
|
|
520
567
|
}
|
|
521
568
|
|
|
569
|
+
// 「换风格(漫画化)」i2i 工作流:LoadImage → Canny → ControlNetApplyAdvanced 锁构图,
|
|
570
|
+
// VAEEncode 原图作起点,KSampler 用较高 denoise 整体转二次元风格。
|
|
571
|
+
function animaStyleI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, controlNetModel, controlNetStrength, denoise) {
|
|
572
|
+
return {
|
|
573
|
+
'44': { class_type: 'UNETLoader', inputs: { unet_name: cfg.unetName, weight_dtype: 'fp8_e4m3fn' } },
|
|
574
|
+
'45': { class_type: 'CLIPLoader', inputs: { clip_name: cfg.clipName, type: 'stable_diffusion', device: 'default' } },
|
|
575
|
+
'15': { class_type: 'VAELoader', inputs: { vae_name: cfg.vaeName } },
|
|
576
|
+
'13': { class_type: 'LoadImage', inputs: { image: inputImage } },
|
|
577
|
+
'30': { class_type: 'VAEEncode', inputs: { pixels: ['13', 0], vae: ['15', 0] } },
|
|
578
|
+
'11': { class_type: 'CLIPTextEncode', inputs: { text: prompt, clip: ['45', 0] } },
|
|
579
|
+
'12': { class_type: 'CLIPTextEncode', inputs: { text: negativePrompt, clip: ['45', 0] } },
|
|
580
|
+
'70': { class_type: 'Canny', inputs: { image: ['13', 0], low_threshold: 0.4, high_threshold: 0.8 } },
|
|
581
|
+
'71': { class_type: 'ControlNetLoader', inputs: { controlnet_name: controlNetModel } },
|
|
582
|
+
'72': { class_type: 'ControlNetApplyAdvanced', inputs: { positive: ['11', 0], negative: ['12', 0], control_net: ['71', 0], image: ['70', 0], strength: controlNetStrength, start_percent: 0, end_percent: 1 } },
|
|
583
|
+
'19': {
|
|
584
|
+
class_type: 'KSampler',
|
|
585
|
+
inputs: {
|
|
586
|
+
model: ['44', 0],
|
|
587
|
+
positive: ['72', 0],
|
|
588
|
+
negative: ['72', 1],
|
|
589
|
+
latent_image: ['30', 0],
|
|
590
|
+
seed,
|
|
591
|
+
steps,
|
|
592
|
+
cfg: cfgVal,
|
|
593
|
+
sampler_name: cfg.samplerName,
|
|
594
|
+
scheduler: cfg.scheduler,
|
|
595
|
+
denoise,
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
'8': { class_type: 'VAEDecodeTiled', inputs: { samples: ['19', 0], vae: ['15', 0], tile_size: 512, overlap: 64, temporal_size: 64, temporal_overlap: 8 } },
|
|
599
|
+
'9': { class_type: 'SaveImage', inputs: { images: ['8', 0], filename_prefix: 'pdraw/anm_i2i_style' } },
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 「换装换姿势」i2i 工作流:LoadImage → AnimaSiglipeEncodeImage → AnimaIPAdapterApply 保角色特征,
|
|
604
|
+
// VAEEncode 原图作起点,KSampler 用中等 denoise 重画服装/姿势/场景。
|
|
605
|
+
function animaOotdI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, ipAdapterPath, ipAdapterWeight, denoise) {
|
|
606
|
+
return {
|
|
607
|
+
'44': { class_type: 'UNETLoader', inputs: { unet_name: cfg.unetName, weight_dtype: 'fp8_e4m3fn' } },
|
|
608
|
+
'45': { class_type: 'CLIPLoader', inputs: { clip_name: cfg.clipName, type: 'stable_diffusion', device: 'default' } },
|
|
609
|
+
'15': { class_type: 'VAELoader', inputs: { vae_name: cfg.vaeName } },
|
|
610
|
+
'13': { class_type: 'LoadImage', inputs: { image: inputImage } },
|
|
611
|
+
'30': { class_type: 'VAEEncode', inputs: { pixels: ['13', 0], vae: ['15', 0] } },
|
|
612
|
+
'11': { class_type: 'CLIPTextEncode', inputs: { text: prompt, clip: ['45', 0] } },
|
|
613
|
+
'12': { class_type: 'CLIPTextEncode', inputs: { text: negativePrompt, clip: ['45', 0] } },
|
|
614
|
+
'80': { class_type: 'AnimaIPAdapterLoader', inputs: { ipadapter_path: ipAdapterPath } },
|
|
615
|
+
'81': { class_type: 'AnimaSiglipeEncodeImage', inputs: { image: ['13', 0] } },
|
|
616
|
+
'82': { class_type: 'AnimaIPAdapterApply', inputs: { model: ['44', 0], ipadapter: ['80', 0], siglip_features: ['81', 0], start_at: 0, end_at: 1, weight: ipAdapterWeight } },
|
|
617
|
+
'19': {
|
|
618
|
+
class_type: 'KSampler',
|
|
619
|
+
inputs: {
|
|
620
|
+
model: ['82', 0],
|
|
621
|
+
positive: ['11', 0],
|
|
622
|
+
negative: ['12', 0],
|
|
623
|
+
latent_image: ['30', 0],
|
|
624
|
+
seed,
|
|
625
|
+
steps,
|
|
626
|
+
cfg: cfgVal,
|
|
627
|
+
sampler_name: cfg.samplerName,
|
|
628
|
+
scheduler: cfg.scheduler,
|
|
629
|
+
denoise,
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
'8': { class_type: 'VAEDecodeTiled', inputs: { samples: ['19', 0], vae: ['15', 0], tile_size: 512, overlap: 64, temporal_size: 64, temporal_overlap: 8 } },
|
|
633
|
+
'9': { class_type: 'SaveImage', inputs: { images: ['8', 0], filename_prefix: 'pdraw/anm_i2i_ootd' } },
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// 根据 i2i 模式与已检测到的节点能力,决定最终工作流与去噪强度。
|
|
638
|
+
// mode:style=换风格(漫画化)/ ootd=换装换姿势 / 其他=基础 img2img
|
|
639
|
+
// caps:{ controlNet: { available, model }, ipAdapter: { available } }(由 detectI2ICapabilities 返回)
|
|
640
|
+
// 返回 { promptBody, kind, denoise },kind 为 plain / controlnet / ipadapter。
|
|
641
|
+
function buildI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, mode, denoiseOverride, caps) {
|
|
642
|
+
const fallbackDenoise = Number(cfg.img2imgDenoise) || 0.55
|
|
643
|
+
if (mode === 'style') {
|
|
644
|
+
if (caps && caps.controlNet && caps.controlNet.available && caps.controlNet.model) {
|
|
645
|
+
const denoise = denoiseOverride != null ? denoiseOverride : (Number(cfg.i2iStyleDenoise) || 0.75)
|
|
646
|
+
return {
|
|
647
|
+
kind: 'controlnet',
|
|
648
|
+
denoise,
|
|
649
|
+
promptBody: animaStyleI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, caps.controlNet.model, Number(cfg.i2iControlNetStrength) || 0.7, denoise),
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// 没有 ControlNet:换风格又不想崩构图,denoise 自动压到 0.5 以内
|
|
653
|
+
const denoise = denoiseOverride != null ? denoiseOverride : Math.min(Number(cfg.i2iStyleDenoise) || 0.75, 0.5)
|
|
654
|
+
return { kind: 'plain', denoise, promptBody: animaI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, denoise) }
|
|
655
|
+
}
|
|
656
|
+
if (mode === 'ootd') {
|
|
657
|
+
if (caps && caps.ipAdapter && caps.ipAdapter.available && String(cfg.i2iIPAdapterPath || '').trim()) {
|
|
658
|
+
const denoise = denoiseOverride != null ? denoiseOverride : (Number(cfg.i2iOotdDenoise) || 0.55)
|
|
659
|
+
return {
|
|
660
|
+
kind: 'ipadapter',
|
|
661
|
+
denoise,
|
|
662
|
+
promptBody: animaOotdI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, String(cfg.i2iIPAdapterPath).trim(), Number(cfg.i2iIPAdapterWeight) || 0.8, denoise),
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const denoise = denoiseOverride != null ? denoiseOverride : (Number(cfg.i2iOotdDenoise) || 0.55)
|
|
666
|
+
return { kind: 'plain', denoise, promptBody: animaI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, denoise) }
|
|
667
|
+
}
|
|
668
|
+
const denoise = denoiseOverride != null ? denoiseOverride : fallbackDenoise
|
|
669
|
+
return { kind: 'plain', denoise, promptBody: animaI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, denoise) }
|
|
670
|
+
}
|
|
671
|
+
|
|
522
672
|
function customWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, explicitSize) {
|
|
523
673
|
const rawPath = path.resolve(__dirname, cfg.customWorkflowPath)
|
|
524
674
|
let raw
|
|
@@ -1447,8 +1597,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1447
1597
|
if (i2iImage && cfg.customWorkflowEnabled && cfg.customWorkflowPath) {
|
|
1448
1598
|
return { ok: false, message: 'i2i(以图生图)暂不支持自定义工作流(customWorkflowEnabled),请关闭后再试。' }
|
|
1449
1599
|
}
|
|
1600
|
+
const i2iOpts = overrides.i2i || {}
|
|
1450
1601
|
const promptBody = i2iImage
|
|
1451
|
-
?
|
|
1602
|
+
? buildI2IWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, i2iImage, i2iOpts.mode || 'plain', i2iOpts.denoise != null ? i2iOpts.denoise : null, i2iOpts.caps || null).promptBody
|
|
1452
1603
|
: buildWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, Boolean(size))
|
|
1453
1604
|
|
|
1454
1605
|
const clientId = crypto.randomUUID()
|
|
@@ -2875,7 +3026,84 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2875
3026
|
return session.text('.i2i-upload-fail', [e.message])
|
|
2876
3027
|
}
|
|
2877
3028
|
if (cfg.outputLogs) logger.info(`[p-draw] i2i ${session.userId} 原图已上传:${uploadName}`)
|
|
2878
|
-
|
|
3029
|
+
|
|
3030
|
+
// 能力检测(ControlNet / Anima IP-Adapter)
|
|
3031
|
+
const caps = await detectI2ICapabilities()
|
|
3032
|
+
|
|
3033
|
+
// 处理模式:i2iMode=ask 时交互询问;style/ootd/plain 直接固定
|
|
3034
|
+
let mode = 'plain'
|
|
3035
|
+
const cfgMode = String(cfg.i2iMode || 'ask').toLowerCase()
|
|
3036
|
+
if (cfgMode === 'style' || cfgMode === 'ootd') {
|
|
3037
|
+
mode = cfgMode
|
|
3038
|
+
} else if (cfgMode === 'ask' && typeof session.prompt === 'function') {
|
|
3039
|
+
const chosen = await askI2IMode(session)
|
|
3040
|
+
if (chosen === 'cancel') return session.text('.i2i-mode-cancelled')
|
|
3041
|
+
if (chosen === null) {
|
|
3042
|
+
await session.send(session.text('.i2i-mode-cancelled'))
|
|
3043
|
+
return ''
|
|
3044
|
+
}
|
|
3045
|
+
mode = chosen
|
|
3046
|
+
if (mode === 'style') await session.send(session.text('.i2i-mode-style'))
|
|
3047
|
+
if (mode === 'ootd') await session.send(session.text('.i2i-mode-ootd'))
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
// 所选模式依赖的节点缺失时给出提示(仍会回退普通 img2img,不中断)
|
|
3051
|
+
const notices = []
|
|
3052
|
+
if (mode === 'style' && (!caps.controlNet.available || !caps.controlNet.model)) {
|
|
3053
|
+
notices.push(session.text('.i2i-no-controlnet'))
|
|
3054
|
+
}
|
|
3055
|
+
if (mode === 'ootd' && (!caps.ipAdapter.available || !String(cfg.i2iIPAdapterPath || '').trim())) {
|
|
3056
|
+
notices.push(session.text('.i2i-no-ipadapter'))
|
|
3057
|
+
}
|
|
3058
|
+
if (notices.length) {
|
|
3059
|
+
try { await session.send(notices.join('\n')) } catch (e) { logger.warn(`发送 i2i 提示失败:${e.message}`) }
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
// 识图:仅当启用、非无优化模式、且已配置 LLM(识图结果会注入提示词优化)时才执行
|
|
3063
|
+
let taggerTags = ''
|
|
3064
|
+
if (cfg.taggerEnabled && cfg.llmModel && cfg.llmBaseUrl && !stripRawPrefix(rawText).raw) {
|
|
3065
|
+
try {
|
|
3066
|
+
taggerTags = await taggerImage(uploadName)
|
|
3067
|
+
if (cfg.outputLogs) logger.info(`[p-draw] i2i ${session.userId} 识图完成:${taggerTags.slice(0, 120)}${taggerTags.length > 120 ? '...' : ''}`)
|
|
3068
|
+
} catch (e) {
|
|
3069
|
+
logger.warn(`识图失败(继续生图):${e.message}`)
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
return await handleGenerate(session, rawText, uploadName, taggerTags, { mode, caps })
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
// 识图预工作流:LoadImage → WD14Tagger|pysssss,读回原图 tags
|
|
3076
|
+
async function taggerImage(uploadName) {
|
|
3077
|
+
const model = String(cfg.taggerModel || 'wd-v1-4-convnext-tagger-v2').trim()
|
|
3078
|
+
const taggerNodeId = '61'
|
|
3079
|
+
const promptBody = {
|
|
3080
|
+
'60': { class_type: 'LoadImage', inputs: { image: uploadName } },
|
|
3081
|
+
[taggerNodeId]: {
|
|
3082
|
+
class_type: 'WD14Tagger|pysssss',
|
|
3083
|
+
inputs: {
|
|
3084
|
+
image: ['60', 0],
|
|
3085
|
+
model,
|
|
3086
|
+
threshold: Number(cfg.taggerThreshold) || 0.35,
|
|
3087
|
+
character_threshold: Number(cfg.taggerCharacterThreshold) || 0.85,
|
|
3088
|
+
replace_underscore: true,
|
|
3089
|
+
trailing_comma: false,
|
|
3090
|
+
exclude_tags: '',
|
|
3091
|
+
},
|
|
3092
|
+
},
|
|
3093
|
+
}
|
|
3094
|
+
const clientId = crypto.randomUUID()
|
|
3095
|
+
const submit = await comfyPost('/prompt', { prompt: promptBody, client_id: clientId }, 20000)
|
|
3096
|
+
const promptId = submit && submit.prompt_id
|
|
3097
|
+
if (!promptId) throw new Error('识图工作流提交失败')
|
|
3098
|
+
const timeoutMs = Math.max(1, parseInt(cfg.timeout) || 300) * 1000
|
|
3099
|
+
const pollMs = Math.max(1, parseInt(cfg.pollInterval) || 2) * 1000
|
|
3100
|
+
const history = await waitComfyResult(ctx, comfyGet, baseUrl(), promptId, clientId, timeoutMs, pollMs)
|
|
3101
|
+
if (!history) throw new Error('识图超时')
|
|
3102
|
+
const nodeOutput = history.outputs && history.outputs[taggerNodeId]
|
|
3103
|
+
const tags = (nodeOutput && Array.isArray(nodeOutput.tags) ? nodeOutput.tags : []).filter(Boolean)
|
|
3104
|
+
const first = String(tags[0] || '').trim()
|
|
3105
|
+
if (!first) throw new Error('识图未返回标签')
|
|
3106
|
+
return first
|
|
2879
3107
|
}
|
|
2880
3108
|
|
|
2881
3109
|
// 从会话消息里取第一张图片并下载为内存 Buffer
|
|
@@ -2925,12 +3153,62 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2925
3153
|
return data.name
|
|
2926
3154
|
}
|
|
2927
3155
|
|
|
2928
|
-
|
|
3156
|
+
// 检测 i2i 可用能力:ControlNet 模型(ControlNetLoader 的 control_net_name 列表)与
|
|
3157
|
+
// Anima IP-Adapter 专用节点(comfyui-anima-ipadapter)。检测失败全部视为不可用(回退普通 img2img)。
|
|
3158
|
+
async function detectI2ICapabilities() {
|
|
3159
|
+
const caps = { controlNet: { available: false, model: null }, ipAdapter: { available: false } }
|
|
3160
|
+
let info
|
|
3161
|
+
try {
|
|
3162
|
+
info = await getObjectInfoCached()
|
|
3163
|
+
} catch (e) {
|
|
3164
|
+
return caps
|
|
3165
|
+
}
|
|
3166
|
+
if (!info || typeof info !== 'object') return caps
|
|
3167
|
+
const cnModels = availableModels(info, 'ControlNetLoader', 'control_net_name')
|
|
3168
|
+
if (cnModels.length) {
|
|
3169
|
+
caps.controlNet.available = true
|
|
3170
|
+
const configured = String(cfg.controlNetModel || '').trim()
|
|
3171
|
+
if (configured && cnModels.includes(configured)) {
|
|
3172
|
+
caps.controlNet.model = configured
|
|
3173
|
+
} else {
|
|
3174
|
+
const norm = (s) => String(s).toLowerCase().replace(/[-_ ]/g, '')
|
|
3175
|
+
const prefers = ['qwen-image-instantx-controlnet-union', 'anima-instantx-controlnet-union', 'instantx-controlnet-union', 'qwen-image-controlnet-union', 'controlnet-union']
|
|
3176
|
+
const pool = cnModels.filter((m) => /qwen|anima|instant|union|canny/i.test(String(m)))
|
|
3177
|
+
const scored = pool.map((m) => ({ m, s: prefers.reduce((acc, p, i) => acc + (norm(m) === norm(p) ? prefers.length - i : 0), 0) }))
|
|
3178
|
+
scored.sort((a, b) => b.s - a.s)
|
|
3179
|
+
caps.controlNet.model = (scored[0] && scored[0].m) || null
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
if (info.AnimaIPAdapterLoader && info.AnimaIPAdapterApply && info.AnimaSiglipeEncodeImage) {
|
|
3183
|
+
caps.ipAdapter.available = true
|
|
3184
|
+
}
|
|
3185
|
+
if (cfg.outputLogs) logger.info(`[p-draw] i2i 能力检测:ControlNet=${JSON.stringify(caps.controlNet)} IPAdapter=${JSON.stringify(caps.ipAdapter)}`)
|
|
3186
|
+
return caps
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
// 交互式询问 i2i 处理模式,返回 'style' / 'ootd' / 'cancel' / null(超时或无法询问时 null)
|
|
3190
|
+
async function askI2IMode(session, i18nAsk) {
|
|
3191
|
+
if (typeof session.prompt !== 'function') return null
|
|
3192
|
+
await session.send(i18nAsk || session.text('.i2i-mode-ask'))
|
|
3193
|
+
const reply = await session.prompt((cfg.i2iAskTimeout || 60) * 1000).catch(() => null)
|
|
3194
|
+
const ans = String((reply && (reply.content != null ? reply.content : reply)) || '').trim()
|
|
3195
|
+
if (/^(1|①|换风格|风格|漫画|漫画化|style|stylechange)$/i.test(ans)) return 'style'
|
|
3196
|
+
if (/^(2|②|换装|换装换姿势|换衣服|换姿势|ootd|outfit)$/i.test(ans)) return 'ootd'
|
|
3197
|
+
if (/^(3|③|取消|不生成|不要|算了|cancel|no)$/i.test(ans)) return 'cancel'
|
|
3198
|
+
if (!ans) return null
|
|
3199
|
+
await session.send(session.text('.i2i-mode-invalid'))
|
|
3200
|
+
return askI2IMode(session, i18nAsk)
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
async function handleGenerate(session, rawText, i2iImage = null, taggerTags = '', i2iOpts = null) {
|
|
2929
3204
|
const USERID = session.userId
|
|
2930
3205
|
const isAdmin = isAdminUser(session)
|
|
2931
3206
|
const unet = await resolveUnet(USERID)
|
|
2932
3207
|
const i2iRule = i2iImage
|
|
2933
3208
|
? '这是以图生图(img2img):原图的构图、主体与色调会被保留。请主要描述你希望发生的变化(风格、服装、表情、场景改造、细节调整等),不要重复描述原图已有的细节。'
|
|
3209
|
+
+ (taggerTags
|
|
3210
|
+
? `\n\n原图内容参考(本地识图自动识别出的标签,用于了解原图已包含的内容;不要照抄全部标签,只参考与用户改动需求相关的部分):\n${taggerTags}`
|
|
3211
|
+
: '')
|
|
2934
3212
|
: ''
|
|
2935
3213
|
if (cfg.outputLogs) {
|
|
2936
3214
|
logger.info(`[p-draw] 请求 userId=${USERID} isAdmin=${isAdmin} adminUsers=${JSON.stringify(cfg.adminUsers || [])} normalizeId=${normalizeId(USERID)}`)
|
|
@@ -2945,8 +3223,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2945
3223
|
const parsedBatch = parseBatchCount(parsedSize.prompt, cfg.batchMax)
|
|
2946
3224
|
// 固定种子解析(--seed:xxx / --seed xxx / --seed=xxx),并从提示词中剥离
|
|
2947
3225
|
const parsedSeed = parseSeed(parsedBatch.prompt)
|
|
2948
|
-
|
|
3226
|
+
// i2i 去噪强度解析(--denoise 0.6 / --去噪 0.4),并从提示词中剥离
|
|
3227
|
+
const parsedDenoise = i2iImage ? parseDenoise(parsedSeed.prompt) : { denoise: null, prompt: parsedSeed.prompt }
|
|
3228
|
+
const text = parsedDenoise.prompt
|
|
2949
3229
|
const seed = parsedSeed.seed
|
|
3230
|
+
const denoise = parsedDenoise.denoise
|
|
2950
3231
|
const count = parsedBatch.count
|
|
2951
3232
|
|
|
2952
3233
|
// P 点校验(按总价 = 张数 × 单价)
|
|
@@ -3041,6 +3322,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
3041
3322
|
if (cfg.outputLogs) logger.info(`[p-draw] ${USERID} 已扣除 ${count * cfg.price} P 点(${count} 张 × ${cfg.price}),余额 ${saving - count * cfg.price}`)
|
|
3042
3323
|
}
|
|
3043
3324
|
|
|
3325
|
+
// i2i 运行参数:模式(style/ootd/plain)、--denoise 覆盖值、检测到的能力
|
|
3326
|
+
const i2iRun = i2iImage
|
|
3327
|
+
? { i2iImage, i2i: { mode: (i2iOpts && i2iOpts.mode) || 'plain', denoise: denoise != null ? denoise : null, caps: (i2iOpts && i2iOpts.caps) || null } }
|
|
3328
|
+
: {}
|
|
3329
|
+
|
|
3044
3330
|
// 队列:预排队全部任务,占满即退回总价
|
|
3045
3331
|
const queuedTasks = []
|
|
3046
3332
|
let firstPosition = null
|
|
@@ -3052,7 +3338,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
3052
3338
|
const optimized = await optimizePrompt(session, userPrompt, true, i2iRule)
|
|
3053
3339
|
p = composePrompt(optimized.prompt || userPrompt, raw).prompt
|
|
3054
3340
|
}
|
|
3055
|
-
return runComfyGenerate(p, parsedSize.size, { unet, seed,
|
|
3341
|
+
return runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun))
|
|
3056
3342
|
})
|
|
3057
3343
|
if (!q.ok) {
|
|
3058
3344
|
if (!isAdmin) await refundP(USERID, count * cfg.price)
|
|
@@ -3108,7 +3394,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
3108
3394
|
if (cfg.queueEnabled) {
|
|
3109
3395
|
try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
3110
3396
|
} else {
|
|
3111
|
-
try { result = await runComfyGenerate(p, parsedSize.size, { unet, seed,
|
|
3397
|
+
try { result = await runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun)) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
3112
3398
|
}
|
|
3113
3399
|
return result
|
|
3114
3400
|
}
|
|
@@ -3283,5 +3569,5 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
3283
3569
|
})
|
|
3284
3570
|
|
|
3285
3571
|
// 暴露内部接口供自动化测试调用(Koishi 忽略 apply 返回值,不影响生产行为)
|
|
3286
|
-
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, handleGenerateI2I, extractImageFromSession, uploadImageToComfyui, animaI2IWorkflow }
|
|
3572
|
+
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, handleGenerateI2I, extractImageFromSession, uploadImageToComfyui, taggerImage, animaI2IWorkflow, animaStyleI2IWorkflow, animaOotdI2IWorkflow, buildI2IWorkflow, detectI2ICapabilities, parseDenoise }
|
|
3287
3573
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -14,7 +14,8 @@ p点-绘图插件,连接本地 ComfyUI 生图,参考 AstrBot 的 [anima 绘
|
|
|
14
14
|
- 尺寸选择:`竖图` `横图` `方图` `长竖图` `宽屏`,或 `1024x1536:描述` / `--尺寸 1216x832`。
|
|
15
15
|
- 画师组管理(创建 / 追加 / 切换 / 查看 / 删除)。
|
|
16
16
|
- 固定角色(`添加角色 名称=tags`)。
|
|
17
|
-
- **以图生图**:`p-draw i2i <描述>` 并在同一条消息附一张原图(文件 / 截图 /
|
|
17
|
+
- **以图生图**:`p-draw i2i <描述>` 并在同一条消息附一张原图(文件 / 截图 / 链接均可),发送后会让用户选择处理方式(可用 `i2iMode` 固定跳过询问):**① 换风格(漫画化)**——保留原图构图,整体转成二次元画风,优先用 ControlNet(Canny 锁构图);**② 换装换姿势**——保留角色长相,重新设计服装 / 姿势 / 场景,优先用 Anima IP-Adapter(保脸);**③ 取消**——不生成。支持 `--denoise 0.6` / `--去噪 0.4` 单独调整强度;未安装对应节点 / 模型时自动回退普通 img2img(纯 `LoadImage → VAEEncode → KSampler(denoise<1)`,**无需修改 ComfyUI**),去噪强度由 `img2imgDenoise` 控制(默认 0.55)。不支持自定义工作流(`customWorkflowEnabled`)时使用 i2i 会提示关闭。
|
|
18
|
+
- **i2i 自动识图**:配置 `taggerEnabled` 后,i2i 会先通过 ComfyUI 的 **WD14 Tagger** 节点对原图识图,把识别出的标签注入提示词优化,让不支持读图的 LLM(如 DeepSeek)也能"看到"原图内容。需要在 ComfyUI `custom_nodes` 安装 [ComfyUI-WD14-Tagger](https://github.com/pythongosssss/ComfyUI-WD14-Tagger),并把模型 `.onnx` + `.csv` 放进 `custom_nodes/ComfyUI-WD14-Tagger/models/`(模型名由 `taggerModel` 指定,默认 `wd-v1-4-convnext-tagger-v2`)。仅当已配置 LLM(`llmBaseUrl`/`llmModel`)且非 `无优化` 模式时才会识图;识图失败会跳过并正常生图。
|
|
18
19
|
- **用户自选模型**:`p-draw 模型` 查看当前与可用模型(自动读取 ComfyUI 的 UNET 列表),`p-draw 模型 <名称>` 切换(支持模糊匹配,如 `anima-aesthetic`),`p-draw 模型 默认` 恢复默认。偏好按用户保存(`p_draw_config.user_models`),单人 / 多人 / 连续图都生效;未自选时用 `unetName` 默认模型。自定义工作流(`customWorkflowEnabled`)不参与模型切换。
|
|
19
20
|
- 画师组与固定角色**保存在数据库 `p_draw_config` 表中**,管理指令不触发插件重载(不会打断正在生成的图,也不会把配置恢复成默认)。数据库中的值在重启后覆盖配置里的同名项。
|
|
20
21
|
- 自定义 ComfyUI 工作流 JSON。
|
|
@@ -51,7 +52,7 @@ plugins:
|
|
|
51
52
|
画图 创建画师组 千代风格=@artist_a, @artist_b,
|
|
52
53
|
画图 切换画师组 千代风格
|
|
53
54
|
画图 添加角色 狐莉=1girl, solo, fox girl
|
|
54
|
-
画图 i2i
|
|
55
|
+
画图 i2i 换成晚礼服,背景换成舞台灯光 --denoise 0.6(同一条消息附原图,随后回复 1=换风格 / 2=换装换姿势 / 3=取消)
|
|
55
56
|
画图 多人 左边若叶睦抱着吉他,右边千早爱音牵着她的手
|
|
56
57
|
画图 多人 若叶睦与千早爱音十指相扣 x2
|
|
57
58
|
画图 连续 圣女:白色长发红瞳、圣洁白裙、端庄微笑 → 发梢染黑、眼神迷离、白裙开裂 → 黑发红瞳、妖艳邪笑、黑色破损礼服
|
|
@@ -75,7 +76,16 @@ plugins:
|
|
|
75
76
|
| `webSearchEnabled` / `tavilyApiKey` / `webSearchMaxResults` / `webSearchDepth` / `webSearchQueryTemplate` | 联网搜索(指令带 `联网`/`搜索` 触发,需 Tavily Key) |
|
|
76
77
|
| `fixedCharacters` / `artistPresets` / `activeArtistPreset` / `styleTags` | 角色、画师组与画风 |
|
|
77
78
|
| `queueEnabled` / `queueMaxRequests` | 生成队列 |
|
|
78
|
-
| `img2imgDenoise` |
|
|
79
|
+
| `img2imgDenoise` | 普通以图生图(`p-draw i2i`)去噪强度,默认 0.55(建议 0.4-0.7,越小越接近原图) |
|
|
80
|
+
| `i2iMode` | i2i 模式选择方式:`ask`=每次询问(默认) / `style`=直接换风格(漫画化) / `ootd`=直接换装换姿势 / `plain`=普通 img2img |
|
|
81
|
+
| `i2iAskTimeout` | i2i 处理方式询问等待时间(秒,默认 60) |
|
|
82
|
+
| `i2iStyleDenoise` | 换风格(漫画化)去噪强度,默认 0.75(建议 0.7-0.85,越大风格变化越彻底;无 ControlNet 时自动压到 0.5 防崩) |
|
|
83
|
+
| `i2iOotdDenoise` | 换装换姿势去噪强度,默认 0.55(建议 0.5-0.6) |
|
|
84
|
+
| `i2iControlNetStrength` | 换风格模式的 ControlNet 强度,默认 0.7(建议 0.5-0.8,越大构图锁得越死) |
|
|
85
|
+
| `controlNetModel` | ControlNet 模型文件名(留空自动检测 Qwen/Anima 系,如 `Qwen-Image-InstantX-ControlNet-Union.safetensors`;需放到 ComfyUI `models/controlnet/`) |
|
|
86
|
+
| `i2iIPAdapterPath` | Anima IP-Adapter 模型文件**路径**(换装换姿势保脸用,如 `E:/anima/ipadapter.safetensors`;需安装 [comfyui-anima-ipadapter](https://github.com/Wenaka2004/comfyui-anima-ipadapter) 节点) |
|
|
87
|
+
| `i2iIPAdapterWeight` | 换装换姿势模式的 IP-Adapter 权重,默认 0.8(建议 0.6-1.0,越大角色特征保留越强) |
|
|
88
|
+
| `taggerEnabled` / `taggerModel` / `taggerThreshold` / `taggerCharacterThreshold` | i2i 自动识图开关、识图模型名、标签阈值 / 角色标签阈值(需 ComfyUI 安装 WD14 Tagger 节点与模型) |
|
|
79
89
|
| `couponPrice` / `couponAskTimeout` | 提示词优化券单价(P 点/张,默认 3000,可被 `data/p-shop.json` 覆盖)与交互确认等待时间(秒,默认 60) |
|
|
80
90
|
| `batchMax` | 单次指令最多生成的张数(`x3` / `3张` / `--数量 3` 等写法),默认 4 |
|
|
81
91
|
| `price` / `multiPrice` / `adminUsers` | 单张 / 多人单张消耗的 P 点与免单管理员 |
|
|
@@ -101,4 +111,5 @@ plugins:
|
|
|
101
111
|
- 券为一次性、**按张数消耗**:`x3` 需 3 张,每张图独立做一次 LLM 优化;券不足时该批不优化也不扣券(提示 `token-short`)。等待回复时间由 `couponAskTimeout`(秒)控制。
|
|
102
112
|
- **管理员在全局关闭时自动免费优化**(不耗券);`无优化` 原样模式不消耗、不询问。平台不支持交互确认时退回自动消耗逻辑。
|
|
103
113
|
- **未使用优化的提示**:单人生图没走 LLM 优化时会明确提示原因——未配置 LLM、未消耗优化券、或 `无优化` 原样模式;多人指令强依赖 LLM 规划,未配置 `llmBaseUrl`/`llmModel` 时会直接提示「多人指令需要 LLM 规划」。
|
|
114
|
+
- **i2i 增强模式依赖的节点**:换风格(漫画化)需要 ComfyUI `models/controlnet/` 里有 Qwen/Anima 系 ControlNet 模型(`ControlNetLoader` 下拉可见);换装换姿势需要安装 [comfyui-anima-ipadapter](https://github.com/Wenaka2004/comfyui-anima-ipadapter) 自定义节点并配置 `i2iIPAdapterPath`。两者缺失时都会自动回退普通 img2img 并提示,不影响出图。
|
|
104
115
|
- 本地模型不一定认识新角色,可用 `添加角色` 手动补充角色 tags。
|