koishi-plugin-p-draw 1.3.6 → 1.4.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 +36 -305
- package/lib/i18n.js +1 -24
- package/lib/parse.js +1 -14
- package/lib/workflows.js +0 -147
- package/package.json +1 -1
- package/readme.md +4 -19
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@ const fs = require('fs')
|
|
|
3
3
|
const fsp = require('fs/promises')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const crypto = require('crypto')
|
|
6
|
-
const { pathToFileURL
|
|
6
|
+
const { pathToFileURL } = require('url')
|
|
7
7
|
|
|
8
8
|
exports.name = 'p-draw'
|
|
9
9
|
|
|
@@ -28,12 +28,7 @@ exports.usage = `
|
|
|
28
28
|
- **联网搜索:** 描述中带 \`联网\` / \`搜索\` / \`查一下\` 等词时,会先联网搜索补充角色设定(需配置 Tavily Key)。
|
|
29
29
|
- **画师组:** \`创建画师组 名称=tags\` \`切换画师组 名称\` \`查看画师组\` \`删除画师组 名称\`
|
|
30
30
|
- **固定角色:** \`添加角色 名称=tags\`
|
|
31
|
-
|
|
32
|
-
- **① 换风格(漫画化)**:保留原图构图,把图片转成二次元画风(用 ControlNet 锁结构)
|
|
33
|
-
- **② 换装换姿势**:保留角色长相,重新设计服装/姿势/场景(用 IP-Adapter 保脸)
|
|
34
|
-
- **③ 取消**:不生成
|
|
35
|
-
- 可加 \`--denoise 0.6\` 单独调整强度;未装 ControlNet/IPAdapter 时自动回退普通 img2img。
|
|
36
|
-
`;
|
|
31
|
+
`;
|
|
37
32
|
|
|
38
33
|
const { zhCN } = require('./lib/i18n')
|
|
39
34
|
exports.Config = Schema.object({
|
|
@@ -48,7 +43,6 @@ exports.Config = Schema.object({
|
|
|
48
43
|
|
|
49
44
|
// 模型文件
|
|
50
45
|
unetName: Schema.string().default('anima-base-v1.0.safetensors').description('主模型文件名(需与 ComfyUI models/diffusion_models 下的文件名完全一致,含连字符;默认 anima-base-v1.0.safetensors)'),
|
|
51
|
-
i2iUnetName: Schema.string().default('anima-base-v1.0.safetensors').description('「换风格(漫画化)」工作流专用主模型文件名。LLLite 权重按 block 数逐块训练,当前 LLLite 权重为 28-block,必须搭配 28-block 模型(anima-base-v1.0 / anima-aesthetic-v1.1);不要用 40-block 的 Anima-2.9B,否则报 depth_embed slices missing。留空则用主模型 unetName'),
|
|
52
46
|
modelParams: Schema.dict(Schema.object({
|
|
53
47
|
samplerName: Schema.string().description('采样器(如 er_sde / res_2s / dpmpp_2m)'),
|
|
54
48
|
scheduler: Schema.string().description('调度器(如 simple / beta57 / normal)'),
|
|
@@ -98,20 +92,7 @@ exports.Config = Schema.object({
|
|
|
98
92
|
multiPrice: Schema.number().default(900).description('多人指令(p-draw 多人)单张消耗的 P 点'),
|
|
99
93
|
couponPrice: Schema.number().default(3000).description('提示词优化券单价(P 点/张,购买询问时显示;可自动读取 data/p-shop.json 里的价格覆盖)'),
|
|
100
94
|
couponAskTimeout: Schema.number().default(60).description('提示词优化券确认等待时间(秒)'),
|
|
101
|
-
|
|
102
|
-
i2iMode: Schema.string().default('ask').description('i2i 模式选择方式:ask=每次询问 / style=直接换风格(漫画化)/ ootd=直接换装换姿势 / plain=普通 img2img'),
|
|
103
|
-
i2iAskTimeout: Schema.number().default(60).description('i2i 模式询问等待时间(秒)'),
|
|
104
|
-
seriesAskTimeout: Schema.number().default(60).description('连续图 LLM 使用确认等待时间(秒)'),
|
|
105
|
-
i2iStyleDenoise: Schema.number().default(0.75).description('换风格模式(漫画化)的去噪强度,越大风格变化越彻底(建议 0.7-0.85)'),
|
|
106
|
-
i2iOotdDenoise: Schema.number().default(0.55).description('换装换姿势模式(保留角色)的去噪强度(建议 0.5-0.6)'),
|
|
107
|
-
i2iControlNetStrength: Schema.number().default(0.7).description('换风格模式的 ControlNet-LLLite 强度,越大构图锁得越死(建议 0.5-0.8;需安装 kohya-ss/ComfyUI-Anima-LLLite 节点与权重)'),
|
|
108
|
-
i2iIPAdapterPath: Schema.string().default('').description('Anima IP-Adapter 模型文件路径(换装换姿势模式保脸用;需安装 comfyui-anima-ipadapter 节点并把模型路径填到这里)'),
|
|
109
|
-
i2iIPAdapterWeight: Schema.number().default(0.8).description('换装换姿势模式的 IP-Adapter 权重,越大角色特征保留越强(建议 0.6-1.0)'),
|
|
110
|
-
controlNetModel: Schema.string().default('').description('Anima ControlNet-LLLite 权重文件名(留空自动检测 anima-lllite 系;需放到 ComfyUI/models/controlnet,如 anima-lllite-lineart-test-1.safetensors)'),
|
|
111
|
-
taggerEnabled: Schema.boolean().default(false).description('i2i 前自动识图(需 ComfyUI 安装 WD14 Tagger 节点与模型;识别出的标签会注入提示词优化)'),
|
|
112
|
-
taggerModel: Schema.string().default('wd-v1-4-convnext-tagger-v2').description('识图模型名(WD14 Tagger 节点里可选模型)'),
|
|
113
|
-
taggerThreshold: Schema.number().default(0.35).description('识图标签置信度阈值'),
|
|
114
|
-
taggerCharacterThreshold: Schema.number().default(0.85).description('识图角色标签置信度阈值'),
|
|
95
|
+
seriesAskTimeout: Schema.number().default(60).description('连续图 LLM 使用确认等待时间(秒)'),
|
|
115
96
|
adminUsers: Schema.array(Schema.string()).default([]).description('免 P 点管理员用户 ID 列表'),
|
|
116
97
|
outputLogs: Schema.boolean().default(true).description('是否在控制台输出详细日志'),
|
|
117
98
|
|
|
@@ -138,7 +119,7 @@ exports.Config = Schema.object({
|
|
|
138
119
|
// 纯函数库已拆分到 lib/(解析 / tag 清洗 / 工作流 / Comfy 等待 / 多人规划 / HTTP 客户端)
|
|
139
120
|
// ------------------------------------------------------------------
|
|
140
121
|
const {
|
|
141
|
-
normalizeBaseUrl, escapeRe, parseGenerationSize, parseBatchCount, parseSeed,
|
|
122
|
+
normalizeBaseUrl, escapeRe, parseGenerationSize, parseBatchCount, parseSeed,
|
|
142
123
|
stripRawPrefix, splitPositiveNegativePrompt, parseNameTags, parsePresetList, mergeTagText,
|
|
143
124
|
} = require('./lib/parse')
|
|
144
125
|
const {
|
|
@@ -146,8 +127,7 @@ const {
|
|
|
146
127
|
NO_ARTIST_RE, NO_STYLE_RE,
|
|
147
128
|
} = require('./lib/tags')
|
|
148
129
|
const {
|
|
149
|
-
|
|
150
|
-
animaOotdI2IWorkflow, buildI2IWorkflow, customWorkflow,
|
|
130
|
+
buildWorkflow,
|
|
151
131
|
} = require('./lib/workflows')
|
|
152
132
|
const { outputImages, waitComfyResult } = require('./lib/comfy')
|
|
153
133
|
const { materializeImageSource } = require('./lib/media')
|
|
@@ -260,16 +240,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
260
240
|
if (objectInfo) {
|
|
261
241
|
const unetList = availableModels(objectInfo, 'UNETLoader', 'unet_name')
|
|
262
242
|
const clipList = availableModels(objectInfo, 'CLIPLoader', 'clip_name')
|
|
263
|
-
const vaeList = availableModels(objectInfo, 'VAELoader', 'vae_name')
|
|
264
|
-
payload.unet_available = unetList.includes(cfg.unetName)
|
|
265
|
-
payload.unet_models = unetList
|
|
266
|
-
payload.
|
|
267
|
-
payload.
|
|
268
|
-
payload.vae_available = vaeList.includes(cfg.vaeName)
|
|
243
|
+
const vaeList = availableModels(objectInfo, 'VAELoader', 'vae_name')
|
|
244
|
+
payload.unet_available = unetList.includes(cfg.unetName)
|
|
245
|
+
payload.unet_models = unetList
|
|
246
|
+
payload.clip_available = clipList.includes(cfg.clipName)
|
|
247
|
+
payload.vae_available = vaeList.includes(cfg.vaeName)
|
|
269
248
|
} else {
|
|
270
|
-
payload.unet_available = undefined
|
|
271
|
-
payload.
|
|
272
|
-
payload.clip_available = undefined
|
|
249
|
+
payload.unet_available = undefined
|
|
250
|
+
payload.clip_available = undefined
|
|
273
251
|
payload.vae_available = undefined
|
|
274
252
|
}
|
|
275
253
|
return payload
|
|
@@ -312,10 +290,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
312
290
|
const lines = [
|
|
313
291
|
`ComfyUI 状态:在线`,
|
|
314
292
|
`版本:${payload.comfyui_version || '未知'}`,
|
|
315
|
-
`GPU:${payload.gpu || '未知'}(显存 ${payload.vram_total_mb}MB / 空闲 ${payload.vram_free_mb}MB)`,
|
|
316
|
-
`主模型:${cfg.unetName} ${modelStatus(cfg.unetName, payload.unet_available)}`,
|
|
317
|
-
|
|
318
|
-
`文本编码器:${cfg.clipName} ${modelStatus(cfg.clipName, payload.clip_available)}`,
|
|
293
|
+
`GPU:${payload.gpu || '未知'}(显存 ${payload.vram_total_mb}MB / 空闲 ${payload.vram_free_mb}MB)`,
|
|
294
|
+
`主模型:${cfg.unetName} ${modelStatus(cfg.unetName, payload.unet_available)}`,
|
|
295
|
+
`文本编码器:${cfg.clipName} ${modelStatus(cfg.clipName, payload.clip_available)}`,
|
|
319
296
|
`VAE:${cfg.vaeName} ${modelStatus(cfg.vaeName, payload.vae_available)}`,
|
|
320
297
|
`可用尺寸:${payload.allowed_sizes.join('、')}`,
|
|
321
298
|
]
|
|
@@ -597,14 +574,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
597
574
|
const cfgVal = Number(overrides.cfg) || workCfg.cfg
|
|
598
575
|
const seed = Number(overrides.seed) || crypto.randomInt(1, 2 ** 32 - 1)
|
|
599
576
|
const negativePrompt = joinPromptParts([overrides.negativePrompt || cfg.negativePrompt || ''])
|
|
600
|
-
const
|
|
601
|
-
if (i2iImage && cfg.customWorkflowEnabled && cfg.customWorkflowPath) {
|
|
602
|
-
return { ok: false, message: 'i2i(以图生图)暂不支持自定义工作流(customWorkflowEnabled),请关闭后再试。' }
|
|
603
|
-
}
|
|
604
|
-
const i2iOpts = overrides.i2i || {}
|
|
605
|
-
const promptBody = i2iImage
|
|
606
|
-
? buildI2IWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, i2iImage, i2iOpts.mode || 'plain', i2iOpts.denoise != null ? i2iOpts.denoise : null, i2iOpts.caps || null).promptBody
|
|
607
|
-
: buildWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, Boolean(size))
|
|
577
|
+
const promptBody = buildWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, Boolean(size))
|
|
608
578
|
|
|
609
579
|
const clientId = crypto.randomUUID()
|
|
610
580
|
const submit = await comfyPost('/prompt', { prompt: promptBody, client_id: clientId }, 20000)
|
|
@@ -771,7 +741,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
771
741
|
}
|
|
772
742
|
}
|
|
773
743
|
|
|
774
|
-
async function optimizePrompt(session, userPrompt, force = false,
|
|
744
|
+
async function optimizePrompt(session, userPrompt, force = false, precomputedSearch = null) {
|
|
775
745
|
if (!cfg.promptOptimizeEnabled && !force) {
|
|
776
746
|
return { ok: true, prompt: userPrompt, reason: 'optimize_disabled' }
|
|
777
747
|
}
|
|
@@ -787,7 +757,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
787
757
|
searchBlock = await webSearch(userPrompt)
|
|
788
758
|
}
|
|
789
759
|
const characterRule = buildCharacterRule(userPrompt)
|
|
790
|
-
const defaultTemplate = `你是为图像生成模型编写正面提示词的 AI 画师。\n\n请根据用户的原始要求设计一幅完整、协调、具有视觉吸引力的画面,并将结果输出为英文 Danbooru-style tags。\n\n输出要求:\n- 只输出一行英文 tags,使用英文逗号分隔。\n- 不要输出解释、分析、标题、编号、Markdown、代码块或中文。\n- 不要输出 masterpiece、best quality、score 等质量前缀。\n- 不要输出画师 tags;质量词和画师组会由程序另行拼接。\n- 尽量使用模型容易理解的可见画面描述。\n- 保持用户明确指定的角色、主体、人数、关键服装、动作、表情和道具。\n- 以最终图像协调、精致、有表现力和好看为优先。\n\n角色和动态上下文:\n{character_rule}\n{
|
|
760
|
+
const defaultTemplate = `你是为图像生成模型编写正面提示词的 AI 画师。\n\n请根据用户的原始要求设计一幅完整、协调、具有视觉吸引力的画面,并将结果输出为英文 Danbooru-style tags。\n\n输出要求:\n- 只输出一行英文 tags,使用英文逗号分隔。\n- 不要输出解释、分析、标题、编号、Markdown、代码块或中文。\n- 不要输出 masterpiece、best quality、score 等质量前缀。\n- 不要输出画师 tags;质量词和画师组会由程序另行拼接。\n- 尽量使用模型容易理解的可见画面描述。\n- 保持用户明确指定的角色、主体、人数、关键服装、动作、表情和道具。\n- 以最终图像协调、精致、有表现力和好看为优先。\n\n角色和动态上下文:\n{character_rule}\n{search_block}\n\n用户原始要求:\n{theme}`
|
|
791
761
|
const template = (cfg.promptOptimizeTemplate || '').trim() || defaultTemplate
|
|
792
762
|
const searchBlockText = searchBlock
|
|
793
763
|
? `联网搜索参考信息(请尽量依据这些内容补全角色外观与设定):\n${searchBlock}`
|
|
@@ -796,10 +766,6 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
796
766
|
.replace(/\{theme\}/g, userPrompt)
|
|
797
767
|
.replace(/\{search_block\}/g, searchBlockText)
|
|
798
768
|
.replace(/\{character_rule\}/g, characterRule)
|
|
799
|
-
.replace(/\{outfit_transfer_rule\}/g, '')
|
|
800
|
-
.replace(/\{reference_rule\}/g, '')
|
|
801
|
-
.replace(/\{img2img_rule\}/g, img2imgRule)
|
|
802
|
-
.replace(/\{sensual_rule\}/g, '')
|
|
803
769
|
.replace(/[ \t]+\n/g, '\n')
|
|
804
770
|
.replace(/\n{3,}/g, '\n\n')
|
|
805
771
|
.trim()
|
|
@@ -1633,8 +1599,8 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1633
1599
|
if (cfg.outputLogs) logger.info(`[p-draw] ${USERID} 连续图已扣除 ${count * price} P 点(${count} 阶段 × ${price},seed=${seed}),余额 ${saving - count * price}`)
|
|
1634
1600
|
}
|
|
1635
1601
|
|
|
1636
|
-
// 队列:预排队全部阶段(先查容量再入队)
|
|
1637
|
-
const queued = await enqueueBatch(count, (i) => runComfyGenerate(stagePrompts[i], size, { seed, unet }), { USERID, isAdmin, totalPrice: count * price })
|
|
1602
|
+
// 队列:预排队全部阶段(先查容量再入队)
|
|
1603
|
+
const queued = await enqueueBatch(count, (i) => runComfyGenerate(stagePrompts[i], size, { seed, unet }), { USERID, isAdmin, totalPrice: count * price })
|
|
1638
1604
|
if (!queued.ok) return queued.message
|
|
1639
1605
|
const queuedTasks = queued.tasks
|
|
1640
1606
|
const firstPosition = queued.firstPosition
|
|
@@ -1650,15 +1616,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1650
1616
|
await sendNotices(session, notice)
|
|
1651
1617
|
await sendNotices(session, [feedback.chargeNotice], { quote: true })
|
|
1652
1618
|
|
|
1653
|
-
// 单阶段生成(共用 seed)
|
|
1654
|
-
const runOne = async (i) => {
|
|
1655
|
-
let result
|
|
1619
|
+
// 单阶段生成(共用 seed)
|
|
1620
|
+
const runOne = async (i) => {
|
|
1621
|
+
let result
|
|
1656
1622
|
if (cfg.queueEnabled) {
|
|
1657
1623
|
try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1658
1624
|
} else {
|
|
1659
1625
|
try { result = await runComfyGenerate(stagePrompts[i], size, { seed, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1660
1626
|
}
|
|
1661
|
-
result.prompt = stagePrompts[i]
|
|
1662
1627
|
return result
|
|
1663
1628
|
}
|
|
1664
1629
|
|
|
@@ -1854,13 +1819,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1854
1819
|
return await handleGenerateSeries(session, seriesMatch[1].trim())
|
|
1855
1820
|
}
|
|
1856
1821
|
|
|
1857
|
-
//
|
|
1858
|
-
const i2iMatch = text.match(/^i2i\s*(.*)$/i)
|
|
1859
|
-
if (i2iMatch) {
|
|
1860
|
-
return await handleGenerateI2I(session, i2iMatch[1].trim())
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// 多人指令:p-draw 多人 <描述>
|
|
1822
|
+
// 多人指令:p-draw 多人 <描述>
|
|
1864
1823
|
const multiMatch = text.match(/^(?:多人|多人生图|双人|三人|群像)\s*(.*)$/)
|
|
1865
1824
|
if (multiMatch) {
|
|
1866
1825
|
return await handleGenerateMulti(session, multiMatch[1].trim())
|
|
@@ -1985,230 +1944,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1985
1944
|
return await handleGenerate(session, text)
|
|
1986
1945
|
})
|
|
1987
1946
|
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
}
|
|
1993
|
-
const image = await extractImageFromSession(session)
|
|
1994
|
-
if (!image) return session.text('.i2i-no-image')
|
|
1995
|
-
let uploadName
|
|
1996
|
-
try {
|
|
1997
|
-
uploadName = await uploadImageToComfyui(image)
|
|
1998
|
-
} catch (e) {
|
|
1999
|
-
logger.warn(`上传原图失败:${e.message}`)
|
|
2000
|
-
return session.text('.i2i-upload-fail', [e.message])
|
|
2001
|
-
}
|
|
2002
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i ${session.userId} 原图已上传:${uploadName}`)
|
|
2003
|
-
|
|
2004
|
-
// 能力检测(ControlNet / Anima IP-Adapter)
|
|
2005
|
-
const caps = await detectI2ICapabilities()
|
|
2006
|
-
|
|
2007
|
-
// 处理模式:i2iMode=ask 时交互询问;style/ootd/plain 直接固定
|
|
2008
|
-
let mode = 'plain'
|
|
2009
|
-
const cfgMode = String(cfg.i2iMode || 'ask').toLowerCase()
|
|
2010
|
-
if (cfgMode === 'style' || cfgMode === 'ootd') {
|
|
2011
|
-
mode = cfgMode
|
|
2012
|
-
} else if (cfgMode === 'ask' && typeof session.prompt === 'function') {
|
|
2013
|
-
const chosen = await askI2IMode(session)
|
|
2014
|
-
if (chosen === 'cancel') return session.text('.i2i-mode-cancelled')
|
|
2015
|
-
if (chosen === null) {
|
|
2016
|
-
await session.send(session.text('.i2i-mode-cancelled'))
|
|
2017
|
-
return ''
|
|
2018
|
-
}
|
|
2019
|
-
mode = chosen
|
|
2020
|
-
if (mode === 'style') await session.send(session.text('.i2i-mode-style'))
|
|
2021
|
-
if (mode === 'ootd') await session.send(session.text('.i2i-mode-ootd'))
|
|
2022
|
-
}
|
|
2023
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i ${session.userId} 模式=${mode} ControlNet=${caps.controlNet.available ? caps.controlNet.model : '-'} IPAdapter=${caps.ipAdapter.available ? 'on' : 'off'}`)
|
|
2024
|
-
|
|
2025
|
-
// 所选模式依赖的节点缺失时给出提示(仍会回退普通 img2img,不中断)
|
|
2026
|
-
const notices = []
|
|
2027
|
-
if (mode === 'style' && (!caps.controlNet.available || !caps.controlNet.model)) {
|
|
2028
|
-
notices.push(session.text('.i2i-no-controlnet'))
|
|
2029
|
-
}
|
|
2030
|
-
if (mode === 'ootd' && (!caps.ipAdapter.available || !String(cfg.i2iIPAdapterPath || '').trim())) {
|
|
2031
|
-
notices.push(session.text('.i2i-no-ipadapter'))
|
|
2032
|
-
}
|
|
2033
|
-
if (notices.length) {
|
|
2034
|
-
try { await session.send(notices.join('\n')) } catch (e) { logger.warn(`发送 i2i 提示失败:${e.message}`) }
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
// 识图:仅当启用、非无优化模式、且已配置 LLM(识图结果会注入提示词优化)时才执行
|
|
2038
|
-
let taggerTags = ''
|
|
2039
|
-
if (cfg.taggerEnabled && cfg.llmModel && cfg.llmBaseUrl && !stripRawPrefix(rawText).raw) {
|
|
2040
|
-
try {
|
|
2041
|
-
taggerTags = await taggerImage(uploadName)
|
|
2042
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i ${session.userId} 识图完成:${taggerTags.slice(0, 120)}${taggerTags.length > 120 ? '...' : ''}`)
|
|
2043
|
-
} catch (e) {
|
|
2044
|
-
logger.warn(`识图失败(继续生图):${e.message}`)
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
return await handleGenerate(session, rawText, uploadName, taggerTags, { mode, caps })
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
// 识图预工作流:LoadImage → WD14Tagger|pysssss,读回原图 tags
|
|
2051
|
-
async function taggerImage(uploadName) {
|
|
2052
|
-
const model = String(cfg.taggerModel || 'wd-v1-4-convnext-tagger-v2').trim()
|
|
2053
|
-
const taggerNodeId = '61'
|
|
2054
|
-
const promptBody = {
|
|
2055
|
-
'60': { class_type: 'LoadImage', inputs: { image: uploadName } },
|
|
2056
|
-
[taggerNodeId]: {
|
|
2057
|
-
class_type: 'WD14Tagger|pysssss',
|
|
2058
|
-
inputs: {
|
|
2059
|
-
image: ['60', 0],
|
|
2060
|
-
model,
|
|
2061
|
-
threshold: Number(cfg.taggerThreshold) || 0.35,
|
|
2062
|
-
character_threshold: Number(cfg.taggerCharacterThreshold) || 0.85,
|
|
2063
|
-
replace_underscore: true,
|
|
2064
|
-
trailing_comma: false,
|
|
2065
|
-
exclude_tags: '',
|
|
2066
|
-
},
|
|
2067
|
-
},
|
|
2068
|
-
}
|
|
2069
|
-
const clientId = crypto.randomUUID()
|
|
2070
|
-
const submit = await comfyPost('/prompt', { prompt: promptBody, client_id: clientId }, 20000)
|
|
2071
|
-
const promptId = submit && submit.prompt_id
|
|
2072
|
-
if (!promptId) throw new Error('识图工作流提交失败')
|
|
2073
|
-
const timeoutMs = Math.max(1, parseInt(cfg.timeout) || 300) * 1000
|
|
2074
|
-
const pollMs = Math.max(1, parseInt(cfg.pollInterval) || 2) * 1000
|
|
2075
|
-
const history = await waitComfyResult(ctx, comfyGet, baseUrl(), promptId, clientId, timeoutMs, pollMs)
|
|
2076
|
-
if (!history) throw new Error('识图超时')
|
|
2077
|
-
const nodeOutput = history.outputs && history.outputs[taggerNodeId]
|
|
2078
|
-
const tags = (nodeOutput && Array.isArray(nodeOutput.tags) ? nodeOutput.tags : []).filter(Boolean)
|
|
2079
|
-
const first = String(tags[0] || '').trim()
|
|
2080
|
-
if (!first) throw new Error('识图未返回标签')
|
|
2081
|
-
return first
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
// 从会话消息里取第一张图片并下载为内存 Buffer
|
|
2085
|
-
async function extractImageFromSession(session) {
|
|
2086
|
-
const elements = session.elements || []
|
|
2087
|
-
const img = elements.find((e) => e.type === 'img' || e.type === 'image')
|
|
2088
|
-
if (!img) return null
|
|
2089
|
-
const attrs = img.attrs || img.data || {}
|
|
2090
|
-
const src = String(attrs.src || '')
|
|
2091
|
-
if (!src) return null
|
|
2092
|
-
try {
|
|
2093
|
-
if (/^file:\/\//i.test(src)) {
|
|
2094
|
-
// 修复:Windows 下 file:///K:/... 用 replace 会得到 /K:/...(无法读取),用 fileURLToPath 解析
|
|
2095
|
-
let filePath
|
|
2096
|
-
try {
|
|
2097
|
-
filePath = fileURLToPath(src)
|
|
2098
|
-
} catch (e) {
|
|
2099
|
-
filePath = src.replace(/^file:\/\//i, '')
|
|
2100
|
-
}
|
|
2101
|
-
const buffer = await fsp.readFile(filePath)
|
|
2102
|
-
return { buffer, ext: path.extname(filePath) || '.png' }
|
|
2103
|
-
}
|
|
2104
|
-
if (/^data:/i.test(src)) {
|
|
2105
|
-
const m = src.match(/^data:image\/([a-zA-Z0-9+]+);base64,(.+)$/)
|
|
2106
|
-
if (!m) return null
|
|
2107
|
-
const kind = m[1].toLowerCase()
|
|
2108
|
-
const ext = kind === 'jpeg' ? '.jpg' : kind === 'webp' ? '.webp' : kind === 'png' ? '.png' : '.' + (kind || 'png')
|
|
2109
|
-
return { buffer: Buffer.from(m[2], 'base64'), ext }
|
|
2110
|
-
}
|
|
2111
|
-
const res = await fetch(src)
|
|
2112
|
-
if (!res.ok) return null
|
|
2113
|
-
const buffer = Buffer.from(await res.arrayBuffer())
|
|
2114
|
-
const mime = String(res.headers.get('content-type') || '')
|
|
2115
|
-
const ext = mime.includes('jpeg') ? '.jpg' : mime.includes('webp') ? '.webp' : '.png'
|
|
2116
|
-
return { buffer, ext }
|
|
2117
|
-
} catch (e) {
|
|
2118
|
-
logger.warn(`读取原图失败:${e.message}`)
|
|
2119
|
-
return null
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
// 上传图片到 ComfyUI input 目录,返回 ComfyUI 使用的文件名
|
|
2124
|
-
async function uploadImageToComfyui(image) {
|
|
2125
|
-
const filename = `pdraw_i2i_${Date.now()}_${crypto.randomBytes(4).toString('hex')}${image.ext || '.png'}`
|
|
2126
|
-
const form = new FormData()
|
|
2127
|
-
form.append('image', new Blob([image.buffer]), filename)
|
|
2128
|
-
form.append('overwrite', 'true')
|
|
2129
|
-
form.append('type', 'input')
|
|
2130
|
-
const res = await fetch(baseUrl() + '/upload/image', { method: 'POST', body: form })
|
|
2131
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
2132
|
-
const data = await res.json().catch(() => ({}))
|
|
2133
|
-
if (!data || !data.name) throw new Error('ComfyUI 未返回上传文件名')
|
|
2134
|
-
return data.name
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
// 检测 i2i 可用能力:Anima ControlNet-LLLite 节点与权重(comfyui-anima-lllite / kohya-ss/ComfyUI-Anima-LLLite)、
|
|
2138
|
-
// LineArt 预处理器(comfyui_controlnet_aux,可选)与 Anima IP-Adapter 专用节点(comfyui-anima-ipadapter)。
|
|
2139
|
-
// 检测失败全部视为不可用(回退普通 img2img)。
|
|
2140
|
-
// 注意:Qwen-Image InstantX ControlNet 与 Anima(MiniTrainDIT,3584 维)架构不兼容,这里只认 LLLite 权重。
|
|
2141
|
-
async function detectI2ICapabilities() {
|
|
2142
|
-
const caps = { controlNet: { available: false, model: null, preprocessor: 'Canny' }, ipAdapter: { available: false } }
|
|
2143
|
-
let info
|
|
2144
|
-
try {
|
|
2145
|
-
info = await getObjectInfoCached()
|
|
2146
|
-
} catch (e) {
|
|
2147
|
-
return caps
|
|
2148
|
-
}
|
|
2149
|
-
if (!info || typeof info !== 'object') return caps
|
|
2150
|
-
// LineArt 预处理器(comfyui_controlnet_aux):AnimeLineArt 更贴近漫画线条,优先;LineArt 次之;都没有则回退 Canny
|
|
2151
|
-
if (info.AnimeLineArtPreprocessor) {
|
|
2152
|
-
caps.controlNet.preprocessor = 'AnimeLineArtPreprocessor'
|
|
2153
|
-
} else if (info.LineArtPreprocessor) {
|
|
2154
|
-
caps.controlNet.preprocessor = 'LineArtPreprocessor'
|
|
2155
|
-
}
|
|
2156
|
-
// Anima ControlNet-LLLite:lllite_name 从 models/controlnet 目录读取,只挑 anima-lllite 系权重
|
|
2157
|
-
if (info.AnimaLLLiteApply_sdscripts) {
|
|
2158
|
-
const llliteModels = availableModels(info, 'AnimaLLLiteApply_sdscripts', 'lllite_name')
|
|
2159
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i LLLite 节点存在,models/controlnet 文件列表=${JSON.stringify(llliteModels)}`)
|
|
2160
|
-
if (llliteModels.length) {
|
|
2161
|
-
const configured = String(cfg.controlNetModel || '').trim()
|
|
2162
|
-
const norm = (s) => String(s).toLowerCase().replace(/[-_ ]/g, '')
|
|
2163
|
-
const prefers = ['anima-lllite-lineart', 'anima-lllite', 'lllite-lineart', 'lllite']
|
|
2164
|
-
const pool = llliteModels.filter((m) => /lllite/i.test(String(m)))
|
|
2165
|
-
const scored = pool.map((m) => ({ m, s: prefers.reduce((acc, p, i) => acc + (norm(m) === norm(p) ? prefers.length - i : 0), 0) }))
|
|
2166
|
-
scored.sort((a, b) => b.s - a.s)
|
|
2167
|
-
const best = (scored[0] && scored[0].m) || null
|
|
2168
|
-
caps.controlNet.model = configured && llliteModels.includes(configured) ? configured : best
|
|
2169
|
-
caps.controlNet.available = !!caps.controlNet.model
|
|
2170
|
-
}
|
|
2171
|
-
// 换风格专用主模型必须是 28-block(与 LLLite 权重匹配),检测它是否已安装
|
|
2172
|
-
const styleModel = String(cfg.i2iUnetName || '').trim() || cfg.unetName
|
|
2173
|
-
const unetList = availableModels(info, 'UNETLoader', 'unet_name')
|
|
2174
|
-
if (unetList.length && !unetList.includes(styleModel)) {
|
|
2175
|
-
logger.warn(`[p-draw] i2i 换风格专用主模型 ${styleModel} 不在 models/diffusion_models 中(现有:${JSON.stringify(unetList)})。LLLite 权重为 28-block,请安装 anima-base-v1.0 或修改 i2iUnetName 配置,否则换风格会报 depth_embed slices missing`)
|
|
2176
|
-
caps.controlNet.modelMismatch = true
|
|
2177
|
-
}
|
|
2178
|
-
} else if (cfg.outputLogs) {
|
|
2179
|
-
logger.info('[p-draw] i2i 未检测到 AnimaLLLiteApply_sdscripts 节点(请确认已安装 kohya-ss/ComfyUI-Anima-LLLite 并重启 ComfyUI)')
|
|
2180
|
-
}
|
|
2181
|
-
if (info.AnimaIPAdapterLoader && info.AnimaIPAdapterApply && info.AnimaSiglipeEncodeImage) {
|
|
2182
|
-
caps.ipAdapter.available = true
|
|
2183
|
-
}
|
|
2184
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i 能力检测:ControlNet(LLLite)=${JSON.stringify(caps.controlNet)} IPAdapter=${JSON.stringify(caps.ipAdapter)}`)
|
|
2185
|
-
return caps
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// 交互式询问 i2i 处理模式,返回 'style' / 'ootd' / 'cancel' / null(超时或无法询问时 null)
|
|
2189
|
-
async function askI2IMode(session, i18nAsk) {
|
|
2190
|
-
if (typeof session.prompt !== 'function') return null
|
|
2191
|
-
await session.send(i18nAsk || session.text('.i2i-mode-ask'))
|
|
2192
|
-
const reply = await session.prompt((cfg.i2iAskTimeout || 60) * 1000).catch(() => null)
|
|
2193
|
-
const ans = String((reply && (reply.content != null ? reply.content : reply)) || '').trim()
|
|
2194
|
-
if (/^(1|①|换风格|风格|漫画|漫画化|style|stylechange)$/i.test(ans)) return 'style'
|
|
2195
|
-
if (/^(2|②|换装|换装换姿势|换衣服|换姿势|ootd|outfit)$/i.test(ans)) return 'ootd'
|
|
2196
|
-
if (/^(3|③|取消|不生成|不要|算了|cancel|no)$/i.test(ans)) return 'cancel'
|
|
2197
|
-
if (!ans) return null
|
|
2198
|
-
await session.send(session.text('.i2i-mode-invalid'))
|
|
2199
|
-
return askI2IMode(session, i18nAsk)
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
async function handleGenerate(session, rawText, i2iImage = null, taggerTags = '', i2iOpts = null) {
|
|
2203
|
-
const USERID = session.userId
|
|
2204
|
-
const isAdmin = isAdminUser(session)
|
|
2205
|
-
const unet = await resolveUnet(USERID)
|
|
2206
|
-
const i2iRule = i2iImage
|
|
2207
|
-
? '这是以图生图(img2img):原图的构图、主体与色调会被保留。请主要描述你希望发生的变化(风格、服装、表情、场景改造、细节调整等),不要重复描述原图已有的细节。'
|
|
2208
|
-
+ (taggerTags
|
|
2209
|
-
? `\n\n原图内容参考(本地识图自动识别出的标签,用于了解原图已包含的内容;不要照抄全部标签,只参考与用户改动需求相关的部分):\n${taggerTags}`
|
|
2210
|
-
: '')
|
|
2211
|
-
: ''
|
|
1947
|
+
async function handleGenerate(session, rawText) {
|
|
1948
|
+
const USERID = session.userId
|
|
1949
|
+
const isAdmin = isAdminUser(session)
|
|
1950
|
+
const unet = await resolveUnet(USERID)
|
|
2212
1951
|
if (cfg.outputLogs) {
|
|
2213
1952
|
logger.info(`[p-draw] 请求 userId=${USERID} isAdmin=${isAdmin} adminUsers=${JSON.stringify(cfg.adminUsers || [])} normalizeId=${normalizeId(USERID)}`)
|
|
2214
1953
|
}
|
|
@@ -2222,12 +1961,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2222
1961
|
const parsedBatch = parseBatchCount(parsedSize.prompt, cfg.batchMax)
|
|
2223
1962
|
// 固定种子解析(--seed:xxx / --seed xxx / --seed=xxx),并从提示词中剥离
|
|
2224
1963
|
const parsedSeed = parseSeed(parsedBatch.prompt)
|
|
2225
|
-
|
|
2226
|
-
const
|
|
2227
|
-
const
|
|
2228
|
-
const seed = parsedSeed.seed
|
|
2229
|
-
const denoise = parsedDenoise.denoise
|
|
2230
|
-
const count = parsedBatch.count
|
|
1964
|
+
const text = parsedSeed.prompt
|
|
1965
|
+
const seed = parsedSeed.seed
|
|
1966
|
+
const count = parsedBatch.count
|
|
2231
1967
|
|
|
2232
1968
|
// P 点校验(按总价 = 张数 × 单价)
|
|
2233
1969
|
// P 点校验(按总价 = 张数 × 单价)
|
|
@@ -2261,13 +1997,13 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2261
1997
|
const tokenOpt = !globalOpt && !isAdmin && cfg.llmModel && cfg.llmBaseUrl
|
|
2262
1998
|
if (globalOpt) {
|
|
2263
1999
|
// 全局优化开启:一次优化,整批复用同一提示词
|
|
2264
|
-
const optimized = await optimizePrompt(session, userPrompt, false
|
|
2000
|
+
const optimized = await optimizePrompt(session, userPrompt, false)
|
|
2265
2001
|
finalPrompt = optimized.prompt
|
|
2266
2002
|
degraded = !optimized.ok
|
|
2267
2003
|
optimizedReason = optimized.reason || ''
|
|
2268
2004
|
} else if (adminOpt) {
|
|
2269
2005
|
// 管理员在全局关闭时也免费优化(不耗券)
|
|
2270
|
-
const optimized = await optimizePrompt(session, userPrompt, true
|
|
2006
|
+
const optimized = await optimizePrompt(session, userPrompt, true)
|
|
2271
2007
|
finalPrompt = optimized.prompt
|
|
2272
2008
|
degraded = !optimized.ok
|
|
2273
2009
|
optimizedReason = optimized.reason || ''
|
|
@@ -2319,14 +2055,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2319
2055
|
if (cfg.outputLogs) logger.info(`[p-draw] ${USERID} 已扣除 ${count * cfg.price} P 点(${count} 张 × ${cfg.price}),余额 ${saving - count * cfg.price}`)
|
|
2320
2056
|
}
|
|
2321
2057
|
|
|
2322
|
-
// i2i 运行参数:模式(style/ootd/plain)、--denoise 覆盖值、检测到的能力
|
|
2323
|
-
const i2iRun = i2iImage
|
|
2324
|
-
? { i2iImage, i2i: { mode: (i2iOpts && i2iOpts.mode) || 'plain', denoise: denoise != null ? denoise : null, caps: (i2iOpts && i2iOpts.caps) || null } }
|
|
2325
|
-
: {}
|
|
2326
2058
|
// 用户手写了 negative: 区块时,默认负面词仍保留;用户 tag 只补充未出现的部分。
|
|
2327
2059
|
const generationOverrides = Object.assign(
|
|
2328
2060
|
{ unet, seed },
|
|
2329
|
-
i2iRun,
|
|
2330
2061
|
userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
|
|
2331
2062
|
)
|
|
2332
2063
|
|
|
@@ -2337,7 +2068,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2337
2068
|
const queued = await enqueueBatch(count, async (i) => {
|
|
2338
2069
|
let p = finalPrompt
|
|
2339
2070
|
if (perImageOptimize) {
|
|
2340
|
-
const optimized = await optimizePrompt(session, userPrompt, true,
|
|
2071
|
+
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
2341
2072
|
p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
|
|
2342
2073
|
}
|
|
2343
2074
|
const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
|
|
@@ -2373,7 +2104,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2373
2104
|
const runOne = async (i) => {
|
|
2374
2105
|
let p = finalPrompt
|
|
2375
2106
|
if (perImageOptimize) {
|
|
2376
|
-
const optimized = await optimizePrompt(session, userPrompt, true,
|
|
2107
|
+
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
2377
2108
|
p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
|
|
2378
2109
|
}
|
|
2379
2110
|
let result
|
|
@@ -2564,5 +2295,5 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2564
2295
|
})
|
|
2565
2296
|
|
|
2566
2297
|
// 暴露内部接口供自动化测试调用(Koishi 忽略 apply 返回值,不影响生产行为)
|
|
2567
|
-
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice,
|
|
2298
|
+
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, sendNotices, sendImagesAsForward, executeBatch }
|
|
2568
2299
|
}
|
package/lib/i18n.js
CHANGED
|
@@ -43,20 +43,7 @@ const zhCN = {
|
|
|
43
43
|
multiPrice: { $description: '多人指令(p-draw 多人)单张消耗的 P 点' },
|
|
44
44
|
couponPrice: { $description: '提示词优化券单价(P 点/张,购买询问时显示)' },
|
|
45
45
|
couponAskTimeout: { $description: '提示词优化券确认等待时间(秒)' },
|
|
46
|
-
img2imgDenoise: { $description: '普通以图生图(p-draw i2i)的去噪强度,越小越接近原图(建议 0.4-0.7)' },
|
|
47
|
-
i2iMode: { $description: 'i2i 模式选择方式:ask=每次询问 / style=直接换风格(漫画化)/ ootd=直接换装换姿势 / plain=普通 img2img' },
|
|
48
|
-
i2iAskTimeout: { $description: 'i2i 模式询问等待时间(秒)' },
|
|
49
46
|
seriesAskTimeout: { $description: '连续图 LLM 使用确认等待时间(秒)' },
|
|
50
|
-
i2iStyleDenoise: { $description: '换风格模式(漫画化)的去噪强度,越大风格变化越彻底(建议 0.7-0.85)' },
|
|
51
|
-
i2iOotdDenoise: { $description: '换装换姿势模式(保留角色)的去噪强度(建议 0.5-0.6)' },
|
|
52
|
-
i2iControlNetStrength: { $description: '换风格模式的 ControlNet 强度,越大构图锁得越死(建议 0.5-0.8)' },
|
|
53
|
-
i2iIPAdapterPath: { $description: 'Anima IP-Adapter 模型文件路径(换装换姿势模式保脸用;需安装 comfyui-anima-ipadapter 节点并把模型路径填到这里)' },
|
|
54
|
-
i2iIPAdapterWeight: { $description: '换装换姿势模式的 IP-Adapter 权重,越大角色特征保留越强(建议 0.6-1.0)' },
|
|
55
|
-
controlNetModel: { $description: 'ControlNet 模型文件名(留空自动检测 Qwen/Anima 系 ControlNet;需放到 ComfyUI/models/controlnet)' },
|
|
56
|
-
taggerEnabled: { $description: 'i2i 前自动识图(需 ComfyUI 安装 WD14 Tagger 节点与模型;识别出的标签会注入提示词优化)' },
|
|
57
|
-
taggerModel: { $description: '识图模型名(WD14 Tagger 节点里可选模型)' },
|
|
58
|
-
taggerThreshold: { $description: '识图标签置信度阈值' },
|
|
59
|
-
taggerCharacterThreshold: { $description: '识图角色标签置信度阈值' },
|
|
60
47
|
adminUsers: { $description: '免 P 点管理员用户 ID 列表' },
|
|
61
48
|
outputLogs: { $description: '是否在控制台输出详细日志' },
|
|
62
49
|
multiVerifyEnabled: { $description: '多人图生成后启用视觉校验(需配置下方视觉模型)' },
|
|
@@ -76,7 +63,7 @@ const zhCN = {
|
|
|
76
63
|
description: '连接本地 ComfyUI 生图,消耗 P 点',
|
|
77
64
|
messages: {
|
|
78
65
|
'not-permitted': 'ComfyUI 助手已关闭,或当前用户没有使用权限。',
|
|
79
|
-
usage: 'p-draw 帮助(本指令名可自行更换,如 /anm):\n\n【生成】\n p-draw <描述>\n
|
|
66
|
+
usage: 'p-draw 帮助(本指令名可自行更换,如 /anm):\n\n【生成】\n p-draw <描述>\n 可加 --seed 数字、x3 / 3张、竖图 / 横图 / 方图\n 无优化 <tags> 可直接使用手写 tags\n\n【多人生成】\n p-draw 多人 <描述>(2-4 人画面,依赖 LLM 规划)\n\n【连续图】\n p-draw 连续 <角色>:<阶段1> → <阶段2> → ...\n 每阶段独立 T2I,共用 seed;阶段分隔:→ / -> / |\n\n【提示词优化】\n 配置 LLM 后可将自然语言转为 Danbooru tags;普通用户可使用提示词优化券\n\n【画师组】\n 创建 / 追加 / 切换 / 查看 / 删除画师组\n\n【固定角色】\n 添加角色 名称=tags\n\n【模型】\n p-draw 模型(查看) / p-draw 模型 <名称>(切换)/ p-draw 模型 默认(重置)\n\n【状态】\n p-draw 状态\n\n生成图片以合并转发发送,并附对应的 Positive / Negative 提示词;扣费提醒单独引用原指令。',
|
|
80
67
|
'account-notExists': '君现在还没有 p 点,请先签到哦',
|
|
81
68
|
'no-enough-p': '君的 p 点不够 {0}p 哦,先去签个到吧qwq',
|
|
82
69
|
'no-prompt': '请提供画面描述,例如:p-draw 一个女孩,白色裙子',
|
|
@@ -133,16 +120,6 @@ const zhCN = {
|
|
|
133
120
|
'model-no-draw': '「模型」只能用来切换/查看模型,不能生图。请先用「p-draw 模型 <名称>」切换,再单独发送要画的内容。',
|
|
134
121
|
'model-ambiguous': '「{0}」匹配到多个模型,请写得更具体些:\n{1}',
|
|
135
122
|
'no-optimize': '提示:本次未使用 LLM 优化({0})。',
|
|
136
|
-
'i2i-no-image': 'i2i(以图生图)需要附一张原图。用法:p-draw i2i <描述>,并在同一条消息里带上图片。',
|
|
137
|
-
'i2i-upload-fail': '原图上传 ComfyUI 失败:{0}',
|
|
138
|
-
'i2i-no-custom-workflow': 'i2i(以图生图)暂不支持自定义工作流(customWorkflowEnabled),请关闭后再试。',
|
|
139
|
-
'i2i-mode-ask': '请选择 i2i 处理模式:\n① 换风格(漫画化)——保留原图构图,转成二次元画风\n② 换装换姿势——保留角色长相,重新设计服装/姿势/场景\n③ 取消——不生成\n回复 1 / 2 / 3 或对应名称即可。',
|
|
140
|
-
'i2i-mode-invalid': '没有理解你的选择。请回复 ①换风格(漫画化) / ②换装换姿势 / ③取消。',
|
|
141
|
-
'i2i-mode-cancelled': '已取消本次 i2i 生图。',
|
|
142
|
-
'i2i-mode-style': '已选择【换风格(漫画化)】:保留原图构图,转成二次元画风。',
|
|
143
|
-
'i2i-mode-ootd': '已选择【换装换姿势】:保留角色长相,重新设计服装/姿势/场景。',
|
|
144
|
-
'i2i-no-controlnet': '未检测到可用的 Anima ControlNet-LLLite,本次「换风格」将使用普通 img2img,构图保留效果会弱一些。',
|
|
145
|
-
'i2i-no-ipadapter': '未检测到可用的 Anima IP-Adapter,本次「换装换姿势」将使用普通 img2img,角色保留效果会弱一些。',
|
|
146
123
|
'multi-no-llm': '多人指令需要 LLM 规划,但当前未配置 llmBaseUrl / llmModel。请管理员在配置中填写后使用。',
|
|
147
124
|
},
|
|
148
125
|
},
|
package/lib/parse.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 解析器纯函数库:地址/尺寸/批量/seed/
|
|
1
|
+
// 解析器纯函数库:地址/尺寸/批量/seed/raw 前缀/tag 合并/预设解析。
|
|
2
2
|
// 不依赖 koishi,可独立单测。
|
|
3
3
|
|
|
4
4
|
// ------------------------------------------------------------------
|
|
@@ -152,18 +152,6 @@ function parseSeed(text) {
|
|
|
152
152
|
return { seed, prompt: cleaned }
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
// 去噪强度解析:--denoise 0.3 / --denoise=0.6 / --去噪 0.4,并从提示词里剥离
|
|
156
|
-
function parseDenoise(text) {
|
|
157
|
-
const prompt = String(text || '').trim()
|
|
158
|
-
const m = prompt.match(/(?<!\S)--(?:denoise|去噪)\s*(?:=|=|:|:)?\s*(\d+(?:\.\d+)?)/i)
|
|
159
|
-
if (!m) return { denoise: null, prompt }
|
|
160
|
-
const denoise = Math.min(1, Math.max(0, parseFloat(m[1])))
|
|
161
|
-
const cleaned = (prompt.slice(0, m.index) + ' ' + prompt.slice(m.index + m[0].length)).trim()
|
|
162
|
-
.replace(/^[\s,,;;::]+|[\s,,;;::]+$/g, '')
|
|
163
|
-
.replace(/\s+/g, ' ')
|
|
164
|
-
return { denoise, prompt: cleaned }
|
|
165
|
-
}
|
|
166
|
-
|
|
167
155
|
// ------------------------------------------------------------------
|
|
168
156
|
// 提示词辅助(移植自 anima prompt_presets)
|
|
169
157
|
// ------------------------------------------------------------------
|
|
@@ -269,7 +257,6 @@ module.exports = {
|
|
|
269
257
|
cnNumValue,
|
|
270
258
|
parseBatchCount,
|
|
271
259
|
parseSeed,
|
|
272
|
-
parseDenoise,
|
|
273
260
|
RAW_PREFIXES,
|
|
274
261
|
stripRawPrefix,
|
|
275
262
|
splitPositiveNegativePrompt,
|
package/lib/workflows.js
CHANGED
|
@@ -39,149 +39,6 @@ function buildWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal
|
|
|
39
39
|
return animaT2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// 以图生图工作流(p-draw i2i):LoadImage → VAEEncode → KSampler(denoise<1)
|
|
43
|
-
// 输入图编码为 latent 作为起点,尺寸保持原图,去噪强度由 cfg.img2imgDenoise 控制。
|
|
44
|
-
function animaI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, denoise) {
|
|
45
|
-
return {
|
|
46
|
-
'44': { class_type: 'UNETLoader', inputs: { unet_name: cfg.unetName, weight_dtype: 'fp8_e4m3fn' } },
|
|
47
|
-
'45': { class_type: 'CLIPLoader', inputs: { clip_name: cfg.clipName, type: 'stable_diffusion', device: 'default' } },
|
|
48
|
-
'15': { class_type: 'VAELoader', inputs: { vae_name: cfg.vaeName } },
|
|
49
|
-
'13': { class_type: 'LoadImage', inputs: { image: inputImage } },
|
|
50
|
-
'30': { class_type: 'VAEEncode', inputs: { pixels: ['13', 0], vae: ['15', 0] } },
|
|
51
|
-
'11': { class_type: 'CLIPTextEncode', inputs: { text: prompt, clip: ['45', 0] } },
|
|
52
|
-
'12': { class_type: 'CLIPTextEncode', inputs: { text: negativePrompt, clip: ['45', 0] } },
|
|
53
|
-
'19': {
|
|
54
|
-
class_type: 'KSampler',
|
|
55
|
-
inputs: {
|
|
56
|
-
model: ['44', 0],
|
|
57
|
-
positive: ['11', 0],
|
|
58
|
-
negative: ['12', 0],
|
|
59
|
-
latent_image: ['30', 0],
|
|
60
|
-
seed,
|
|
61
|
-
steps,
|
|
62
|
-
cfg: cfgVal,
|
|
63
|
-
sampler_name: cfg.samplerName,
|
|
64
|
-
scheduler: cfg.scheduler,
|
|
65
|
-
denoise,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
'8': { class_type: 'VAEDecodeTiled', inputs: { samples: ['19', 0], vae: ['15', 0], tile_size: 512, overlap: 64, temporal_size: 64, temporal_overlap: 8 } },
|
|
69
|
-
'9': { class_type: 'SaveImage', inputs: { images: ['8', 0], filename_prefix: 'pdraw/anm_i2i' } },
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// 「换风格(漫画化)」i2i 工作流:LoadImage → 预处理器(LineArt / Canny)→ AnimaLLLiteApply_sdscripts 锁构图,
|
|
74
|
-
// VAEEncode 原图作起点,KSampler 用较高 denoise 整体转二次元风格。
|
|
75
|
-
// 注意:Anima 是 MiniTrainDIT 架构(隐藏层 3584 维),与 Qwen-Image 系 ControlNet 不兼容
|
|
76
|
-
// (InstantX 控制网期望 3584 维文本嵌入,Anima 只给 1024 维 → LayerNorm 崩溃)。
|
|
77
|
-
// 因此这里使用 Anima 原生支持的 ControlNet-LLLite(kohya-ss/ComfyUI-Anima-LLLite),
|
|
78
|
-
// 节点直接对 MODEL 打补丁并返回补丁后的模型,交给 KSampler。
|
|
79
|
-
// 预处理器:comfyui_controlnet_aux 的 AnimeLineArt/LineArt 节点(需安装该自定义节点);
|
|
80
|
-
// 未安装时回退 ComfyUI 内置 Canny。anima-lllite-lineart 权重是按 lineart 训练的,用 LineArt 比 Canny 更贴。
|
|
81
|
-
function animaStyleI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, controlNetModel, controlNetStrength, denoise, preprocessor) {
|
|
82
|
-
const detectRes = Math.max(width, height)
|
|
83
|
-
const preprocessorNode = preprocessor === 'AnimeLineArtPreprocessor' || preprocessor === 'LineArtPreprocessor'
|
|
84
|
-
? { class_type: preprocessor, inputs: { image: ['13', 0], detect_resolution: detectRes, resolution: detectRes } }
|
|
85
|
-
: { class_type: 'Canny', inputs: { image: ['13', 0], low_threshold: 0.4, high_threshold: 0.8 } }
|
|
86
|
-
return {
|
|
87
|
-
'44': { class_type: 'UNETLoader', inputs: { unet_name: cfg.i2iUnetName || cfg.unetName, weight_dtype: 'fp8_e4m3fn' } },
|
|
88
|
-
'45': { class_type: 'CLIPLoader', inputs: { clip_name: cfg.clipName, type: 'stable_diffusion', device: 'default' } },
|
|
89
|
-
'15': { class_type: 'VAELoader', inputs: { vae_name: cfg.vaeName } },
|
|
90
|
-
'13': { class_type: 'LoadImage', inputs: { image: inputImage } },
|
|
91
|
-
'30': { class_type: 'VAEEncode', inputs: { pixels: ['13', 0], vae: ['15', 0] } },
|
|
92
|
-
'11': { class_type: 'CLIPTextEncode', inputs: { text: prompt, clip: ['45', 0] } },
|
|
93
|
-
'12': { class_type: 'CLIPTextEncode', inputs: { text: negativePrompt, clip: ['45', 0] } },
|
|
94
|
-
'70': preprocessorNode,
|
|
95
|
-
'71': { class_type: 'AnimaLLLiteApply_sdscripts', inputs: { model: ['44', 0], lllite_name: controlNetModel, image: ['70', 0], strength: controlNetStrength, start_percent: 0, end_percent: 1, preserve_wrapper: true } },
|
|
96
|
-
'19': {
|
|
97
|
-
class_type: 'KSampler',
|
|
98
|
-
inputs: {
|
|
99
|
-
model: ['71', 0],
|
|
100
|
-
positive: ['11', 0],
|
|
101
|
-
negative: ['12', 0],
|
|
102
|
-
latent_image: ['30', 0],
|
|
103
|
-
seed,
|
|
104
|
-
steps,
|
|
105
|
-
cfg: cfgVal,
|
|
106
|
-
sampler_name: cfg.samplerName,
|
|
107
|
-
scheduler: cfg.scheduler,
|
|
108
|
-
denoise,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
'8': { class_type: 'VAEDecodeTiled', inputs: { samples: ['19', 0], vae: ['15', 0], tile_size: 512, overlap: 64, temporal_size: 64, temporal_overlap: 8 } },
|
|
112
|
-
'9': { class_type: 'SaveImage', inputs: { images: ['8', 0], filename_prefix: 'pdraw/anm_i2i_style' } },
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// 「换装换姿势」i2i 工作流:LoadImage → AnimaSiglipeEncodeImage → AnimaIPAdapterApply 保角色特征,
|
|
117
|
-
// VAEEncode 原图作起点,KSampler 用中等 denoise 重画服装/姿势/场景。
|
|
118
|
-
function animaOotdI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, ipAdapterPath, ipAdapterWeight, denoise) {
|
|
119
|
-
return {
|
|
120
|
-
'44': { class_type: 'UNETLoader', inputs: { unet_name: cfg.unetName, weight_dtype: 'fp8_e4m3fn' } },
|
|
121
|
-
'45': { class_type: 'CLIPLoader', inputs: { clip_name: cfg.clipName, type: 'stable_diffusion', device: 'default' } },
|
|
122
|
-
'15': { class_type: 'VAELoader', inputs: { vae_name: cfg.vaeName } },
|
|
123
|
-
'13': { class_type: 'LoadImage', inputs: { image: inputImage } },
|
|
124
|
-
'30': { class_type: 'VAEEncode', inputs: { pixels: ['13', 0], vae: ['15', 0] } },
|
|
125
|
-
'11': { class_type: 'CLIPTextEncode', inputs: { text: prompt, clip: ['45', 0] } },
|
|
126
|
-
'12': { class_type: 'CLIPTextEncode', inputs: { text: negativePrompt, clip: ['45', 0] } },
|
|
127
|
-
'80': { class_type: 'AnimaIPAdapterLoader', inputs: { ipadapter_path: ipAdapterPath } },
|
|
128
|
-
'81': { class_type: 'AnimaSiglipeEncodeImage', inputs: { image: ['13', 0] } },
|
|
129
|
-
'82': { class_type: 'AnimaIPAdapterApply', inputs: { model: ['44', 0], ipadapter: ['80', 0], siglip_features: ['81', 0], start_at: 0, end_at: 1, weight: ipAdapterWeight } },
|
|
130
|
-
'19': {
|
|
131
|
-
class_type: 'KSampler',
|
|
132
|
-
inputs: {
|
|
133
|
-
model: ['82', 0],
|
|
134
|
-
positive: ['11', 0],
|
|
135
|
-
negative: ['12', 0],
|
|
136
|
-
latent_image: ['30', 0],
|
|
137
|
-
seed,
|
|
138
|
-
steps,
|
|
139
|
-
cfg: cfgVal,
|
|
140
|
-
sampler_name: cfg.samplerName,
|
|
141
|
-
scheduler: cfg.scheduler,
|
|
142
|
-
denoise,
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
'8': { class_type: 'VAEDecodeTiled', inputs: { samples: ['19', 0], vae: ['15', 0], tile_size: 512, overlap: 64, temporal_size: 64, temporal_overlap: 8 } },
|
|
146
|
-
'9': { class_type: 'SaveImage', inputs: { images: ['8', 0], filename_prefix: 'pdraw/anm_i2i_ootd' } },
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 根据 i2i 模式与已检测到的节点能力,决定最终工作流与去噪强度。
|
|
151
|
-
// mode:style=换风格(漫画化)/ ootd=换装换姿势 / 其他=基础 img2img
|
|
152
|
-
// caps:{ controlNet: { available, model }, ipAdapter: { available } }(由 detectI2ICapabilities 返回)
|
|
153
|
-
// 返回 { promptBody, kind, denoise },kind 为 plain / controlnet / ipadapter。
|
|
154
|
-
function buildI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, mode, denoiseOverride, caps) {
|
|
155
|
-
const fallbackDenoise = Number(cfg.img2imgDenoise) || 0.55
|
|
156
|
-
if (mode === 'style') {
|
|
157
|
-
if (caps && caps.controlNet && caps.controlNet.available && caps.controlNet.model) {
|
|
158
|
-
const denoise = denoiseOverride != null ? denoiseOverride : (Number(cfg.i2iStyleDenoise) || 0.75)
|
|
159
|
-
return {
|
|
160
|
-
kind: 'controlnet',
|
|
161
|
-
denoise,
|
|
162
|
-
promptBody: animaStyleI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, caps.controlNet.model, Number(cfg.i2iControlNetStrength) || 0.7, denoise, caps.controlNet.preprocessor),
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// 没有 ControlNet:换风格又不想崩构图,denoise 自动压到 0.5 以内
|
|
166
|
-
const denoise = denoiseOverride != null ? denoiseOverride : Math.min(Number(cfg.i2iStyleDenoise) || 0.75, 0.5)
|
|
167
|
-
return { kind: 'plain', denoise, promptBody: animaI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, denoise) }
|
|
168
|
-
}
|
|
169
|
-
if (mode === 'ootd') {
|
|
170
|
-
if (caps && caps.ipAdapter && caps.ipAdapter.available && String(cfg.i2iIPAdapterPath || '').trim()) {
|
|
171
|
-
const denoise = denoiseOverride != null ? denoiseOverride : (Number(cfg.i2iOotdDenoise) || 0.55)
|
|
172
|
-
return {
|
|
173
|
-
kind: 'ipadapter',
|
|
174
|
-
denoise,
|
|
175
|
-
promptBody: animaOotdI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, String(cfg.i2iIPAdapterPath).trim(), Number(cfg.i2iIPAdapterWeight) || 0.8, denoise),
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const denoise = denoiseOverride != null ? denoiseOverride : (Number(cfg.i2iOotdDenoise) || 0.55)
|
|
179
|
-
return { kind: 'plain', denoise, promptBody: animaI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, denoise) }
|
|
180
|
-
}
|
|
181
|
-
const denoise = denoiseOverride != null ? denoiseOverride : fallbackDenoise
|
|
182
|
-
return { kind: 'plain', denoise, promptBody: animaI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, denoise) }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
42
|
function customWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, explicitSize) {
|
|
186
43
|
// lib/ 子目录下运行,customWorkflowPath 相对插件根目录,故需要回退一层
|
|
187
44
|
const rawPath = path.resolve(__dirname, '..', cfg.customWorkflowPath)
|
|
@@ -276,10 +133,6 @@ function conditioningTextNodeIds(workflow, inputName, textNodes) {
|
|
|
276
133
|
module.exports = {
|
|
277
134
|
animaT2IWorkflow,
|
|
278
135
|
buildWorkflow,
|
|
279
|
-
animaI2IWorkflow,
|
|
280
|
-
animaStyleI2IWorkflow,
|
|
281
|
-
animaOotdI2IWorkflow,
|
|
282
|
-
buildI2IWorkflow,
|
|
283
136
|
customWorkflow,
|
|
284
137
|
conditioningTextNodeIds,
|
|
285
138
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -6,7 +6,7 @@ p点-绘图插件,连接本地 ComfyUI 生图,参考 AstrBot 的 [anima 绘
|
|
|
6
6
|
|
|
7
7
|
- `p-draw <描述>`(别名:画图 / 生图 / 绘图 / 画画)文生图。
|
|
8
8
|
- `p-draw 多人 <描述>` 多人生图(2-4 人画面):LLM 结构化规划人物分组、互动关系与统一场景,未指定尺寸时按人数/肢体接触自动选横图,生成后可选视觉校验。
|
|
9
|
-
- `p-draw 连续 <角色>:<阶段1> → <阶段2> → ...`
|
|
9
|
+
- `p-draw 连续 <角色>:<阶段1> → <阶段2> → ...` 连续图/过程图(同一角色的变化过程):每阶段独立使用 T2I、全阶段共用同一 seed,角色外观尽量一致,默认横图,支持 `|` 分隔与 `--seed` 覆盖。**仅保留 `连续` 一个别名**。
|
|
10
10
|
- 批量张数:描述后加 `x3` / `×3` / `3张` / `三张` / `--数量 3` 可一次生成多张(上限 `batchMax`,默认 4),按张数 × 单价扣 P 点。
|
|
11
11
|
- 自然语言自动优化为 Danbooru tags(可选,需配置 LLM 接口)。
|
|
12
12
|
- 联网搜索:描述中带 `联网`/`搜索`/`查一下` 等词时自动补充角色设定(需 Tavily Key)。
|
|
@@ -14,15 +14,13 @@ p点-绘图插件,连接本地 ComfyUI 生图,参考 AstrBot 的 [anima 绘
|
|
|
14
14
|
- 尺寸选择:`竖图` `横图` `方图` `长竖图` `宽屏`,或 `1024x1536:描述` / `--尺寸 1216x832`。
|
|
15
15
|
- 画师组管理(创建 / 追加 / 切换 / 查看 / 删除)。
|
|
16
16
|
- 固定角色(`添加角色 名称=tags`)。
|
|
17
|
-
- **以图生图**:`p-draw i2i <描述>` 并在同一条消息附一张原图(文件 / 截图 / 链接均可),发送后会让用户选择处理方式(可用 `i2iMode` 固定跳过询问):**① 换风格(漫画化)**——保留原图构图,整体转成二次元画风,优先用 Anima **ControlNet-LLLite**(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`)且非 `无优化` 模式时才会识图;识图失败会跳过并正常生图。
|
|
19
17
|
- **用户自选模型**:`p-draw 模型` 查看当前与可用模型(自动读取 ComfyUI 的 UNET 列表),`p-draw 模型 <名称>` 切换(支持模糊匹配,如 `anima-aesthetic`),`p-draw 模型 默认` 恢复默认。偏好按用户保存(`p_draw_config.user_models`),单人 / 多人 / 连续图都生效;未自选时用 `unetName` 默认模型。自定义工作流(`customWorkflowEnabled`)不参与模型切换。
|
|
20
18
|
- 画师组与固定角色**保存在数据库 `p_draw_config` 表中**,管理指令不触发插件重载(不会打断正在生成的图,也不会把配置恢复成默认)。数据库中的值在重启后覆盖配置里的同名项。
|
|
21
19
|
- 自定义 ComfyUI 工作流 JSON。
|
|
22
20
|
- 生成队列(逐张顺序执行,防止争抢 GPU)。
|
|
23
21
|
- `p-draw 状态` 查看 ComfyUI 连接状态与模型可用性。
|
|
24
22
|
- `p-draw help`(或 `帮助`)查看全部指令帮助,包含生成 / 多人 / 批量张数 / 尺寸 / 画师组 / 状态等。
|
|
25
|
-
-
|
|
23
|
+
- 发图时会以合并转发发送,每张图片后附对应的 Positive / Negative 提示词;扣费提醒单独引用触发指令。
|
|
26
24
|
|
|
27
25
|
## 安装
|
|
28
26
|
|
|
@@ -52,7 +50,6 @@ plugins:
|
|
|
52
50
|
画图 创建画师组 千代风格=@artist_a, @artist_b,
|
|
53
51
|
画图 切换画师组 千代风格
|
|
54
52
|
画图 添加角色 狐莉=1girl, solo, fox girl
|
|
55
|
-
画图 i2i 换成晚礼服,背景换成舞台灯光 --denoise 0.6(同一条消息附原图,随后回复 1=换风格 / 2=换装换姿势 / 3=取消)
|
|
56
53
|
画图 多人 左边若叶睦抱着吉他,右边千早爱音牵着她的手
|
|
57
54
|
画图 多人 若叶睦与千早爱音十指相扣 x2
|
|
58
55
|
画图 连续 圣女:白色长发红瞳、圣洁白裙、端庄微笑 → 发梢染黑、眼神迷离、白裙开裂 → 黑发红瞳、妖艳邪笑、黑色破损礼服
|
|
@@ -67,27 +64,16 @@ plugins:
|
|
|
67
64
|
|----|------|
|
|
68
65
|
| `comfyuiBaseUrl` | ComfyUI API 地址(默认 `http://127.0.0.1:8188`) |
|
|
69
66
|
| `unetName` / `clipName` / `vaeName` | 模型文件名,需与 ComfyUI 下拉框中的文件名一致 |
|
|
70
|
-
| `i2iUnetName` | 「换风格(漫画化)」专用主模型文件名,默认 `anima-base-v1.0.safetensors`。LLLite 权重按 block 数逐块训练(当前权重为 28-block),必须配 28-block 模型(`anima-base-v1.0` / `anima-aesthetic-v1.1`);**不要用 40-block 的 `Anima-2.9B`**,否则报 `depth_embed slices missing`。留空则用 `unetName` |
|
|
71
67
|
| `modelParams` | 每个模型独立参数覆盖(字典):`key`=模型文件名,`value`=要覆盖的 `samplerName` / `scheduler` / `steps` / `cfg` / `width` / `height`。让不同模型用各自最佳采样参数,只填需要覆盖的字段,命令级 `--steps`/`--cfg` 仍优先 |
|
|
72
68
|
| `customWorkflowEnabled` / `customWorkflowPath` | 使用自定义工作流 JSON(相对插件目录) |
|
|
73
69
|
| `width` / `height` / `allowedSizes` | 默认尺寸与可用尺寸列表 |
|
|
74
70
|
| `steps` / `cfg` / `samplerName` / `scheduler` | 采样参数 |
|
|
75
71
|
| `qualityPrefix` / `negativePrompt` | 质量词前缀与负面提示词 |
|
|
76
72
|
| `promptOptimizeEnabled` / `llmBaseUrl` / `llmApiKey` / `llmModel` | 自然语言优化(OpenAI 兼容接口) |
|
|
77
|
-
| `promptOptimizeTemplate` | 自然语言优化模板(支持 `{theme}` `{search_block}` `{character_rule}`
|
|
73
|
+
| `promptOptimizeTemplate` | 自然语言优化模板(支持 `{theme}` `{search_block}` `{character_rule}` 占位符) |
|
|
78
74
|
| `webSearchEnabled` / `tavilyApiKey` / `webSearchMaxResults` / `webSearchDepth` / `webSearchQueryTemplate` | 联网搜索(指令带 `联网`/`搜索` 触发,需 Tavily Key) |
|
|
79
75
|
| `fixedCharacters` / `artistPresets` / `activeArtistPreset` / `styleTags` | 角色、画师组与画风 |
|
|
80
76
|
| `queueEnabled` / `queueMaxRequests` | 生成队列 |
|
|
81
|
-
| `img2imgDenoise` | 普通以图生图(`p-draw i2i`)去噪强度,默认 0.55(建议 0.4-0.7,越小越接近原图) |
|
|
82
|
-
| `i2iMode` | i2i 模式选择方式:`ask`=每次询问(默认) / `style`=直接换风格(漫画化) / `ootd`=直接换装换姿势 / `plain`=普通 img2img |
|
|
83
|
-
| `i2iAskTimeout` | i2i 处理方式询问等待时间(秒,默认 60) |
|
|
84
|
-
| `i2iStyleDenoise` | 换风格(漫画化)去噪强度,默认 0.75(建议 0.7-0.85,越大风格变化越彻底;无 ControlNet 时自动压到 0.5 防崩) |
|
|
85
|
-
| `i2iOotdDenoise` | 换装换姿势去噪强度,默认 0.55(建议 0.5-0.6) |
|
|
86
|
-
| `i2iControlNetStrength` | 换风格模式的 ControlNet-LLLite 强度,默认 0.7(建议 0.5-0.8,越大构图锁得越死) |
|
|
87
|
-
| `controlNetModel` | Anima ControlNet-LLLite 权重文件名(留空自动检测 `anima-lllite` 系,如 `anima-lllite-lineart-test-1.safetensors`;需放到 ComfyUI `models/controlnet/`) |
|
|
88
|
-
| `i2iIPAdapterPath` | Anima IP-Adapter 模型文件**路径**(换装换姿势保脸用,如 `E:/anima/ipadapter.safetensors`;需安装 [comfyui-anima-ipadapter](https://github.com/Wenaka2004/comfyui-anima-ipadapter) 节点) |
|
|
89
|
-
| `i2iIPAdapterWeight` | 换装换姿势模式的 IP-Adapter 权重,默认 0.8(建议 0.6-1.0,越大角色特征保留越强) |
|
|
90
|
-
| `taggerEnabled` / `taggerModel` / `taggerThreshold` / `taggerCharacterThreshold` | i2i 自动识图开关、识图模型名、标签阈值 / 角色标签阈值(需 ComfyUI 安装 WD14 Tagger 节点与模型) |
|
|
91
77
|
| `couponPrice` / `couponAskTimeout` | 提示词优化券单价(P 点/张,默认 3000,可被 `data/p-shop.json` 覆盖)与交互确认等待时间(秒,默认 60) |
|
|
92
78
|
| `batchMax` | 单次指令最多生成的张数(`x3` / `3张` / `--数量 3` 等写法),默认 4 |
|
|
93
79
|
| `price` / `multiPrice` / `adminUsers` | 单张 / 多人单张消耗的 P 点与免单管理员 |
|
|
@@ -105,7 +91,7 @@ plugins:
|
|
|
105
91
|
- 生成失败会自动退回 P 点。
|
|
106
92
|
- 固定种子 `--seed 数字`(支持 `--seed:` `--seed=` `--seed=` 形式)在**单张 / 批量 / 连续图**里都可用,且会从提示词中自动剥离,不会混进 tags。
|
|
107
93
|
- 多人指令的视觉校验需要单独配置视觉模型(`verifyLlm*`),未配置时会跳过校验直接发送。
|
|
108
|
-
-
|
|
94
|
+
- 连续图每阶段独立使用 T2I 并共用 seed,固定角色的身份 tags(角色名 / 种族 / 尖耳朵等)会**始终注入每一阶段**,保证图跟角色相关;阶段描述里明确改变的外观(发色/瞳色/种族变化等)会自动从固定 tags 中剔除,避免"固定银发把黑化阶段拉回去"。
|
|
109
95
|
- **连续图配置了 LLM(`llmBaseUrl`/`llmModel`)时会交互式询问是否使用 LLM 逐阶段优化**:回复 `1 / 是` 使用 LLM(把中文描述转成 Danbooru tags),回复 `2 / 否` 不使用 LLM(直接按你输入的内容生图,适合已写好英文 tags 的情况),回复 `3 / 取消` 则不生成。等待时间由 `seriesAskTimeout`(秒)控制;未配置 LLM 或平台不支持交互时,配置了 LLM 就自动逐阶段优化。anima 是 tag 模型,中文不翻译就体现不到画面里。阶段描述尽量写**能看见的变化**(发色渐变、眼神、服装破损、光环变黑、表情),心理/抽象描述("感到厌恶""沉迷")模型很难画出来。
|
|
110
96
|
- **提示词优化券**:当全局 `promptOptimizeEnabled` 关闭时,普通用户生图会**交互式询问**是否使用券(记录在 `p_system.llmToken`):
|
|
111
97
|
- 有券 → 询问是否使用;确认后消耗券并 LLM 优化,**拒绝则取消本次生图**(不扣 P);
|
|
@@ -113,5 +99,4 @@ plugins:
|
|
|
113
99
|
- 券为一次性、**按张数消耗**:`x3` 需 3 张,每张图独立做一次 LLM 优化;券不足时该批不优化也不扣券(提示 `token-short`)。等待回复时间由 `couponAskTimeout`(秒)控制。
|
|
114
100
|
- **管理员在全局关闭时自动免费优化**(不耗券);`无优化` 原样模式不消耗、不询问。平台不支持交互确认时退回自动消耗逻辑。
|
|
115
101
|
- **未使用优化的提示**:单人生图没走 LLM 优化时会明确提示原因——未配置 LLM、未消耗优化券、或 `无优化` 原样模式;多人指令强依赖 LLM 规划,未配置 `llmBaseUrl`/`llmModel` 时会直接提示「多人指令需要 LLM 规划」。
|
|
116
|
-
- **i2i 增强模式依赖的节点**:换风格(漫画化)需要 ComfyUI 安装 [kohya-ss/ComfyUI-Anima-LLLite](https://github.com/kohya-ss/ComfyUI-Anima-LLLite) 自定义节点,并在 `models/controlnet/` 放一个 `anima-lllite` 系权重(`AnimaLLLiteApply_sdscripts` 节点下拉可见)。**LLLite 权重按 block 数逐块训练,必须与所用模型一致:当前权重为 28-block,换风格工作流默认用 `i2iUnetName`(`anima-base-v1.0`,28-block);若用 40-block 的 `Anima-2.9B` 会报 `depth_embed slices missing for module idx(es) [28..39]`。****注意:Anima 是 MiniTrainDIT 架构(3584 维),与 Qwen-Image 系 ControlNet(如 `Qwen-Image-InstantX-ControlNet-Union`)不兼容,装了也不会被本插件使用。** 可选安装 [comfyui_controlnet_aux](https://github.com/Fannovel16/comfyui_controlnet_aux) 获得 LineArt 预处理器(优先 `AnimeLineArtPreprocessor`,其次 `LineArtPreprocessor`),线条比内置 Canny 更贴合 lllite-lineart 权重;未安装时自动回退 Canny。换装换姿势需要安装 [comfyui-anima-ipadapter](https://github.com/Wenaka2004/comfyui-anima-ipadapter) 自定义节点并配置 `i2iIPAdapterPath`。以上节点缺失时都会自动回退普通 img2img 并提示,不影响出图。
|
|
117
102
|
- 本地模型不一定认识新角色,可用 `添加角色` 手动补充角色 tags。
|