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 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, fileURLToPath } = require('url')
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
- - **以图生图:** \`p-draw i2i <描述>\`,并在同一条消息里附一张原图(文件/截图/链接均可)。发送后会询问处理模式:
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
- img2imgDenoise: Schema.number().default(0.55).description('普通以图生图(p-draw i2i)的去噪强度,越小越接近原图(建议 0.4-0.7)'),
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, parseDenoise,
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
- animaT2IWorkflow, buildWorkflow, animaI2IWorkflow, animaStyleI2IWorkflow,
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.i2i_unet_available = unetList.includes(String(cfg.i2iUnetName || '').trim() || cfg.unetName)
267
- payload.clip_available = clipList.includes(cfg.clipName)
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.i2i_unet_available = undefined
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
- `换风格模型:${cfg.i2iUnetName || cfg.unetName} ${modelStatus(cfg.i2iUnetName || cfg.unetName, payload.i2i_unet_available)}`,
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 i2iImage = overrides.i2iImage
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, img2imgRule = '', precomputedSearch = null) {
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{img2img_rule}\n{search_block}\n\n用户原始要求:\n{theme}`
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
- // 以图生图指令:p-draw i2i <描述>(需同一条消息附带原图)
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
- // ---------------- 以图生图(p-draw i2i) ----------------
1989
- async function handleGenerateI2I(session, rawText) {
1990
- if (cfg.customWorkflowEnabled && cfg.customWorkflowPath) {
1991
- return session.text('.i2i-no-custom-workflow')
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
- // i2i 去噪强度解析(--denoise 0.6 / --去噪 0.4),并从提示词中剥离
2226
- const parsedDenoise = i2iImage ? parseDenoise(parsedSeed.prompt) : { denoise: null, prompt: parsedSeed.prompt }
2227
- const text = parsedDenoise.prompt
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, i2iRule)
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, i2iRule)
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, i2iRule, searchCache)
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, i2iRule, searchCache)
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, handleGenerateI2I, extractImageFromSession, uploadImageToComfyui, taggerImage, animaI2IWorkflow, animaStyleI2IWorkflow, animaOotdI2IWorkflow, buildI2IWorkflow, detectI2ICapabilities, parseDenoise, sendNotices, sendImagesAsForward, executeBatch }
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 例: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 连接状态与模型可用性)',
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/denoise/raw 前缀/tag 合并/预设解析。
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-p-draw",
3
3
  "description": "p点-绘图,连接本地 ComfyUI 生图。支持自然语言优化、尺寸选择、画师组、固定角色、生成队列,需要配合 p-qiandao 使用",
4
- "version": "1.3.6",
4
+ "version": "1.4.0",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js",
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> → ...` 连续图/过程图(同一角色的变化过程):全阶段共用同一 seed,角色外观尽量一致,默认横图,支持 `|` 分隔与 `--seed` 覆盖。**仅保留 `连续` 一个别名**。
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}` `{img2img_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
- - 连续图用「同一角色 + 全阶段共用 seed」保证一致性。固定角色的身份 tags(角色名 / 种族 / 尖耳朵等)会**始终注入每一阶段**,保证图跟角色相关;阶段描述里明确改变的外观(发色/瞳色/种族变化等)会自动从固定 tags 中剔除,避免"固定银发把黑化阶段拉回去"。
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。