koishi-plugin-p-draw 1.2.11 → 1.2.13

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.
Files changed (3) hide show
  1. package/index.js +43 -10
  2. package/package.json +1 -1
  3. package/readme.md +1 -1
package/index.js CHANGED
@@ -81,6 +81,7 @@ const zhCN = {
81
81
  img2imgDenoise: { $description: '普通以图生图(p-draw i2i)的去噪强度,越小越接近原图(建议 0.4-0.7)' },
82
82
  i2iMode: { $description: 'i2i 模式选择方式:ask=每次询问 / style=直接换风格(漫画化)/ ootd=直接换装换姿势 / plain=普通 img2img' },
83
83
  i2iAskTimeout: { $description: 'i2i 模式询问等待时间(秒)' },
84
+ seriesAskTimeout: { $description: '连续图 LLM 使用确认等待时间(秒)' },
84
85
  i2iStyleDenoise: { $description: '换风格模式(漫画化)的去噪强度,越大风格变化越彻底(建议 0.7-0.85)' },
85
86
  i2iOotdDenoise: { $description: '换装换姿势模式(保留角色)的去噪强度(建议 0.5-0.6)' },
86
87
  i2iControlNetStrength: { $description: '换风格模式的 ControlNet 强度,越大构图锁得越死(建议 0.5-0.8)' },
@@ -156,6 +157,9 @@ const zhCN = {
156
157
  'multi-degraded': '多人视觉校验不可用{0},本次已直接发送生成结果。',
157
158
  'multi-verify-error': '多人视觉校验调用失败:{0}',
158
159
  'series-usage': '连续图:p-draw 连续 <角色>:<阶段1> → <阶段2> → ...\n例:p-draw 连续 少女:清纯校服 → 换上晚礼服 → 华丽登场\n或用 | 分隔,可加 --seed 固定种子保证角色一致。',
160
+ 'series-llm-ask': '是否使用 LLM 优化连续图各阶段提示词?\n1. 是(使用 LLM,自动整理成 Danbooru tags)\n2. 否(不使用 LLM,直接用你输入的内容)\n3. 取消(不生成)\n请回复 1 / 2 / 3',
161
+ 'series-llm-invalid': '未识别的回答,请回复 1(使用 LLM)/ 2(不使用 LLM)/ 3(取消)。',
162
+ 'series-no-llm': '本次未使用 LLM 优化,直接使用你输入的描述/tags。',
159
163
  'series-ok': '已扣除 {0} P 点,共 {1} 张连续图(seed={2})',
160
164
  'model-usage': '当前模型:{0}\n可用模型:\n{1}\n用法:p-draw 模型 <名称>(支持模糊匹配,如 anima-aesthetic);p-draw 模型 默认 恢复默认。',
161
165
  'model-switched': '已切换为模型「{0}」,对之后的生图生效。',
@@ -245,6 +249,7 @@ exports.Config = Schema.object({
245
249
  img2imgDenoise: Schema.number().default(0.55).description('普通以图生图(p-draw i2i)的去噪强度,越小越接近原图(建议 0.4-0.7)'),
246
250
  i2iMode: Schema.string().default('ask').description('i2i 模式选择方式:ask=每次询问 / style=直接换风格(漫画化)/ ootd=直接换装换姿势 / plain=普通 img2img'),
247
251
  i2iAskTimeout: Schema.number().default(60).description('i2i 模式询问等待时间(秒)'),
252
+ seriesAskTimeout: Schema.number().default(60).description('连续图 LLM 使用确认等待时间(秒)'),
248
253
  i2iStyleDenoise: Schema.number().default(0.75).description('换风格模式(漫画化)的去噪强度,越大风格变化越彻底(建议 0.7-0.85)'),
249
254
  i2iOotdDenoise: Schema.number().default(0.55).description('换装换姿势模式(保留角色)的去噪强度(建议 0.5-0.6)'),
250
255
  i2iControlNetStrength: Schema.number().default(0.7).description('换风格模式的 ControlNet-LLLite 强度,越大构图锁得越死(建议 0.5-0.8;需安装 kohya-ss/ComfyUI-Anima-LLLite 节点与权重)'),
@@ -1257,7 +1262,7 @@ function parseMultiPersonPlan(text) {
1257
1262
  function renderMultiPersonCharacter(character, opts = {}) {
1258
1263
  const {
1259
1264
  alias = '', resolvedIdentity = '', fixedTags = '', groupedContact = false,
1260
- explicitPositions = false, identityAnchors = [], includePose = true,
1265
+ explicitPositions = false, identityAnchors = [], includePose = true, asTagStream = false,
1261
1266
  } = opts
1262
1267
  let label = String(alias || character.visual_label || character.role || '').trim()
1263
1268
  if (explicitPositions && character.slot) label = `${character.slot} ${label}`
@@ -1278,6 +1283,7 @@ function renderMultiPersonCharacter(character, opts = {}) {
1278
1283
  if (includePose && character.pose) details.push(character.pose)
1279
1284
  if (character.props && character.props.length) details.push(...character.props)
1280
1285
  const joined = details.filter(Boolean).join(', ')
1286
+ if (asTagStream) return [label, joined].filter(Boolean).join(', ')
1281
1287
  return `${label}: ${joined}.`
1282
1288
  }
1283
1289
 
@@ -2120,7 +2126,7 @@ exports.apply = async function apply(ctx, cfg) {
2120
2126
  const aliases = ['Character A', 'Character B', 'Character C', 'Character D']
2121
2127
  const characterCount = plan.characters.length
2122
2128
  const characterRoles = []
2123
- const characterBlocks = []
2129
+ const characterTagStream = []
2124
2130
  const characterEntityNames = []
2125
2131
  let groupedContact = plan.spatial_mode === 'shared_contact'
2126
2132
  const explicitPositionRequested = /左边|右边|左侧|右侧|前景|后方|前后站位|\bon\s+the\s+(?:left|right)\b|\bforeground\b|\bbackground\b/i.test(prompt)
@@ -2188,7 +2194,7 @@ exports.apply = async function apply(ctx, cfg) {
2188
2194
  )
2189
2195
 
2190
2196
  characterEntityNames.push(new Set([character.name, character.danbooru_candidate, fixedName, resolvedIdentity].filter(Boolean)))
2191
- characterBlocks.push(renderMultiPersonCharacter(character, {
2197
+ characterTagStream.push(renderMultiPersonCharacter(character, {
2192
2198
  alias: visualLabel,
2193
2199
  resolvedIdentity,
2194
2200
  fixedTags: fixedName ? fixedTags : '',
@@ -2196,6 +2202,7 @@ exports.apply = async function apply(ctx, cfg) {
2196
2202
  explicitPositions: spatialMode === 'explicit_positions',
2197
2203
  identityAnchors: renderedIdentityTags,
2198
2204
  includePose: !groupedContact,
2205
+ asTagStream: true,
2199
2206
  }))
2200
2207
  }
2201
2208
 
@@ -2269,7 +2276,6 @@ exports.apply = async function apply(ctx, cfg) {
2269
2276
  return displayed
2270
2277
  })
2271
2278
 
2272
- const sceneGuard = 'The composition shows one shared continuous moment.'
2273
2279
  let relativePosition = ''
2274
2280
  if (spatialMode === 'explicit_positions' && characterCount === 2) {
2275
2281
  const slotAliases = {}
@@ -2281,9 +2287,9 @@ exports.apply = async function apply(ctx, cfg) {
2281
2287
  }
2282
2288
  }
2283
2289
 
2284
- const narrativeBlocks = [...characterBlocks, ...displayInteractions, relativePosition, sceneGuard].filter(Boolean)
2290
+ const narrativeBlocks = [...displayInteractions, relativePosition].filter(Boolean)
2285
2291
 
2286
- // 组装最终提示词:质量词 + 画师组 + content + narrative
2292
+ // 组装最终提示词:质量词 + 画师组 + tag 流(含角色外观)+ narrative 块(仅互动/站位)
2287
2293
  const contentClean = cleanContentTags(commonContent, 65, false, [], true)
2288
2294
  const parts = []
2289
2295
  if (cfg.qualityPrefix) parts.push(String(cfg.qualityPrefix).trim())
@@ -2297,6 +2303,7 @@ exports.apply = async function apply(ctx, cfg) {
2297
2303
  if (artistTags) parts.push(artistTags)
2298
2304
  if (cfg.styleTags) parts.push(String(cfg.styleTags).trim())
2299
2305
  parts.push(contentClean || commonContent)
2306
+ if (characterTagStream.length) parts.push(characterTagStream.join(', '))
2300
2307
  let finalPrompt = joinPromptParts(parts)
2301
2308
  if (narrativeBlocks.length) finalPrompt += '\n\n' + narrativeBlocks.join('\n\n')
2302
2309
  return { ok: true, prompt: finalPrompt }
@@ -2666,6 +2673,20 @@ exports.apply = async function apply(ctx, cfg) {
2666
2673
 
2667
2674
  // 连续图/过程图主流程:同一角色多阶段(固定身份 + 阶段描述 + 全阶段共用同一 seed 保证一致性)
2668
2675
  // 语法:连续 <角色>:<阶段1> → <阶段2> → ... 或 连续 <角色>:<阶段1>|<阶段2>|...
2676
+ // 询问是否使用 LLM 优化,返回 'yes' / 'no' / 'cancel' / null(无法交互或超时)
2677
+ async function askSeriesLLMUse(session) {
2678
+ if (typeof session.prompt !== 'function') return null
2679
+ await session.send(session.text('.series-llm-ask'))
2680
+ const reply = await session.prompt((cfg.seriesAskTimeout || 60) * 1000).catch(() => null)
2681
+ const ans = String((reply && (reply.content != null ? reply.content : reply)) || '').trim()
2682
+ if (/^(是|1|①|用|使用|使用llm|yes|y)$/i.test(ans)) return 'yes'
2683
+ if (/^(否|2|②|不用|不使用|不使用llm|不优化|不用llm|no|n)$/i.test(ans)) return 'no'
2684
+ if (/^(取消|3|③|算了|不生成|cancel|c)$/i.test(ans)) return 'cancel'
2685
+ if (!ans) return null
2686
+ await session.send(session.text('.series-llm-invalid'))
2687
+ return askSeriesLLMUse(session)
2688
+ }
2689
+
2669
2690
  async function handleGenerateSeries(session, rawText) {
2670
2691
  const USERID = session.userId
2671
2692
  const isAdmin = isAdminUser(session)
@@ -2723,16 +2744,27 @@ exports.apply = async function apply(ctx, cfg) {
2723
2744
  const ready = await ensureComfyuiReady()
2724
2745
  if (!ready.ok) return ready.message
2725
2746
 
2747
+ // 询问是否使用 LLM 优化:是=用 LLM / 否=直接使用原始描述 / 取消=不生成。
2748
+ // 未配置 LLM 或平台不支持交互式询问时,沿用原有「配置了 LLM 就逐阶段优化」行为。
2749
+ let useLLM = true
2750
+ if (cfg.llmModel && cfg.llmBaseUrl && typeof session.prompt === 'function') {
2751
+ const choice = await askSeriesLLMUse(session)
2752
+ if (choice === 'cancel') return ''
2753
+ if (choice === 'no') useLLM = false
2754
+ }
2755
+
2726
2756
  // 组装各阶段提示词:身份(含固定角色 tags)+ 阶段描述。
2727
- // 连续图**只要配置了 LLM 就强制逐阶段优化**(不受 promptOptimizeEnabled 限制),
2728
- // 因为 anima 是 Danbooru-tag 模型,中文阶段描述必须转成 tags 才能体现在画面里;
2729
- // 未配置 LLM 时退回原始中文描述。
2757
+ // 使用 LLM 时逐阶段优化(不受 promptOptimizeEnabled 限制),因为 anima 是
2758
+ // Danbooru-tag 模型,中文阶段描述必须转成 tags 才能体现在画面里;
2759
+ // 用户选择「否」或未配置 LLM 时,直接使用各阶段原始描述(不注入身份前缀)。
2730
2760
  const fixedChars = parsePresetList(cfg.fixedCharacters)
2731
2761
  const stagePrompts = []
2732
2762
  let degradedStages = 0
2733
2763
  for (const st of stageList) {
2734
2764
  const base = identity ? `${identity},${st}` : st
2735
- const result = await optimizeSeriesStage(base, identity)
2765
+ const result = useLLM
2766
+ ? await optimizeSeriesStage(base, identity)
2767
+ : { ok: true, prompt: st, drops: [], reason: 'user_skipped_llm' }
2736
2768
  if (!result.ok) degradedStages += 1
2737
2769
  const stageTags = result.prompt || base
2738
2770
  const drops = result.drops || []
@@ -2769,6 +2801,7 @@ exports.apply = async function apply(ctx, cfg) {
2769
2801
  const notice = []
2770
2802
  if (clamped) notice.push(session.text('.batch-limit', [count]))
2771
2803
  if (identity && fixedChars[identity]) notice.push(`已固定角色「${identity}」的身份 tags,各阶段外观将保持一致。`)
2804
+ if (!useLLM) notice.push(session.text('.series-no-llm'))
2772
2805
  if (degradedStages) notice.push(session.text('.prompt-degraded', ['(连续图阶段优化失败,已使用原始描述)']))
2773
2806
  if (cfg.queueEnabled) {
2774
2807
  notice.push(session.text('.queued', [firstPosition, cfg.queueMaxRequests || '∞']))
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.2.11",
4
+ "version": "1.2.13",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js",
package/readme.md CHANGED
@@ -106,7 +106,7 @@ plugins:
106
106
  - 固定种子 `--seed 数字`(支持 `--seed:` `--seed=` `--seed=` 形式)在**单张 / 批量 / 连续图**里都可用,且会从提示词中自动剥离,不会混进 tags。
107
107
  - 多人指令的视觉校验需要单独配置视觉模型(`verifyLlm*`),未配置时会跳过校验直接发送。
108
108
  - 连续图用「同一角色 + 全阶段共用 seed」保证一致性。固定角色的身份 tags(角色名 / 种族 / 尖耳朵等)会**始终注入每一阶段**,保证图跟角色相关;阶段描述里明确改变的外观(发色/瞳色/种族变化等)会自动从固定 tags 中剔除,避免"固定银发把黑化阶段拉回去"。
109
- - **连续图只要配置了 LLM(`llmBaseUrl`/`llmModel`)就会强制逐阶段把中文描述转成 Danbooru tags**,不受 `promptOptimizeEnabled` 开关影响——anima 是 tag 模型,中文不翻译就体现不到画面里;未配置 LLM 时退回原始描述并提示。阶段描述尽量写**能看见的变化**(发色渐变、眼神、服装破损、光环变黑、表情),心理/抽象描述("感到厌恶""沉迷")模型很难画出来。
109
+ - **连续图配置了 LLM(`llmBaseUrl`/`llmModel`)时会交互式询问是否使用 LLM 逐阶段优化**:回复 `1 / 是` 使用 LLM(把中文描述转成 Danbooru tags),回复 `2 / 否` 不使用 LLM(直接按你输入的内容生图,适合已写好英文 tags 的情况),回复 `3 / 取消` 则不生成。等待时间由 `seriesAskTimeout`(秒)控制;未配置 LLM 或平台不支持交互时,配置了 LLM 就自动逐阶段优化。anima 是 tag 模型,中文不翻译就体现不到画面里。阶段描述尽量写**能看见的变化**(发色渐变、眼神、服装破损、光环变黑、表情),心理/抽象描述("感到厌恶""沉迷")模型很难画出来。
110
110
  - **提示词优化券**:当全局 `promptOptimizeEnabled` 关闭时,普通用户生图会**交互式询问**是否使用券(记录在 `p_system.llmToken`):
111
111
  - 有券 → 询问是否使用;确认后消耗券并 LLM 优化,**拒绝则取消本次生图**(不扣 P);
112
112
  - 没券/券不足 → 询问是否购买并显示价格(默认 `couponPrice` 3000 P/张,也可自动读取 `data/p-shop.json` 里覆盖的价格);确认后扣 P 购买并消耗、优化生图;**拒绝一次会再次警告**(提示不用券图可能不好看)并再问一次,**再次拒绝则直接生图**(不优化)。**P 点不足买不起券时,会询问是否仍然生图**(回复「否」则取消本次生图)。