koishi-plugin-p-draw 1.3.3 → 1.3.6
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 +50 -39
- package/lib/parse.js +14 -0
- package/lib/tags.js +6 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -139,10 +139,10 @@ exports.Config = Schema.object({
|
|
|
139
139
|
// ------------------------------------------------------------------
|
|
140
140
|
const {
|
|
141
141
|
normalizeBaseUrl, escapeRe, parseGenerationSize, parseBatchCount, parseSeed, parseDenoise,
|
|
142
|
-
stripRawPrefix, parseNameTags, parsePresetList, mergeTagText,
|
|
142
|
+
stripRawPrefix, splitPositiveNegativePrompt, parseNameTags, parsePresetList, mergeTagText,
|
|
143
143
|
} = require('./lib/parse')
|
|
144
144
|
const {
|
|
145
|
-
splitTags, canonicalTagText, joinPromptParts, cleanContentTags, appendInlineProtectedTags,
|
|
145
|
+
splitTags, canonicalTagText, joinPromptParts, mergeNegativePrompts, cleanContentTags, appendInlineProtectedTags,
|
|
146
146
|
NO_ARTIST_RE, NO_STYLE_RE,
|
|
147
147
|
} = require('./lib/tags')
|
|
148
148
|
const {
|
|
@@ -488,21 +488,21 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
//
|
|
491
|
+
// 生成图片使用合并转发;每张图后紧跟实际使用的正负面提示词,不附原消息引用。
|
|
492
492
|
async function sendImagesAsForward(session, outputs) {
|
|
493
493
|
const nodes = []
|
|
494
494
|
const fallback = []
|
|
495
495
|
for (const output of outputs) {
|
|
496
496
|
const src = typeof output === 'string' ? output : output.src
|
|
497
497
|
const prompt = typeof output === 'string' ? '' : String(output.prompt || '')
|
|
498
|
+
const negativePrompt = typeof output === 'string' ? '' : String(output.negativePrompt || '')
|
|
498
499
|
const materialized = await materializeImageSource(src)
|
|
499
500
|
const image = Buffer.isBuffer(materialized) ? h.image(materialized) : h.image(src)
|
|
500
501
|
nodes.push(h('message', image))
|
|
501
502
|
fallback.push(image)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
503
|
+
const promptText = `Positive:\n${prompt}\n\nNegative:\n${negativePrompt}`
|
|
504
|
+
nodes.push(h('message', promptText))
|
|
505
|
+
fallback.push(promptText)
|
|
506
506
|
}
|
|
507
507
|
try {
|
|
508
508
|
await session.send(h('figure', nodes))
|
|
@@ -664,9 +664,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
664
664
|
width,
|
|
665
665
|
height,
|
|
666
666
|
steps,
|
|
667
|
-
cfg: cfgVal,
|
|
668
|
-
prompt_id: promptId,
|
|
669
|
-
|
|
667
|
+
cfg: cfgVal,
|
|
668
|
+
prompt_id: promptId,
|
|
669
|
+
negativePrompt,
|
|
670
|
+
}
|
|
670
671
|
}
|
|
671
672
|
|
|
672
673
|
// ---------------- LLM 提示词优化 ----------------
|
|
@@ -1195,11 +1196,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1195
1196
|
|
|
1196
1197
|
// 视觉校验(anima_verify + generation_verifier 移植):对生成的图片跑视觉 LLM,
|
|
1197
1198
|
// 不合格则用相同提示词重试(最多 multiCandidateCount 张),按多候选规则挑选并返回结果。
|
|
1198
|
-
async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet) {
|
|
1199
|
+
async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet, negativePrompt) {
|
|
1199
1200
|
const verifyBaseUrl = String(cfg.verifyLlmBaseUrl || '').trim()
|
|
1200
1201
|
const verifyModel = String(cfg.verifyLlmModel || '').trim()
|
|
1201
1202
|
if (!verifyBaseUrl || !verifyModel) {
|
|
1202
|
-
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt }
|
|
1203
|
+
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt, negativePrompt }
|
|
1203
1204
|
}
|
|
1204
1205
|
const passScore = Math.max(0, Math.min(10, parseInt(cfg.multiVerifyPassScore) || 6))
|
|
1205
1206
|
const candidateCount = Math.max(1, Math.min(3, parseInt(cfg.multiCandidateCount) || 2))
|
|
@@ -1207,9 +1208,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1207
1208
|
const systemPrompt = buildVerifySystemPrompt(true, planCount)
|
|
1208
1209
|
const candidates = []
|
|
1209
1210
|
let lastVerdict = null
|
|
1210
|
-
let retries = 0
|
|
1211
|
-
let currentImages = images
|
|
1212
|
-
let currentPrompt = prompt
|
|
1211
|
+
let retries = 0
|
|
1212
|
+
let currentImages = images
|
|
1213
|
+
let currentPrompt = prompt
|
|
1214
|
+
let currentNegativePrompt = negativePrompt
|
|
1213
1215
|
|
|
1214
1216
|
async function verifyOnce(imgs, userReq) {
|
|
1215
1217
|
const controller = new AbortController()
|
|
@@ -1307,15 +1309,15 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1307
1309
|
data = extractVerifyJson(reply)
|
|
1308
1310
|
} catch (e) {
|
|
1309
1311
|
logger.warn(`多人视觉校验失败:${e.message}`)
|
|
1310
|
-
return { ok: true, degraded: true, message: session.text('.multi-verify-error', [String(e && e.message || e)]), verdict: null, outputs: images, prompt: currentPrompt }
|
|
1312
|
+
return { ok: true, degraded: true, message: session.text('.multi-verify-error', [String(e && e.message || e)]), verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
|
|
1311
1313
|
}
|
|
1312
1314
|
if (!data) {
|
|
1313
1315
|
logger.warn(`多人视觉校验返回无法解析:${reply.slice(0, 200)}`)
|
|
1314
|
-
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt }
|
|
1316
|
+
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
|
|
1315
1317
|
}
|
|
1316
1318
|
const verdict = verdictFromData(data)
|
|
1317
1319
|
verdict.skipped = false
|
|
1318
|
-
candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt })
|
|
1320
|
+
candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt, negativePrompt: currentNegativePrompt })
|
|
1319
1321
|
selectedOutputs = currentImages
|
|
1320
1322
|
selectedVerdict = verdict
|
|
1321
1323
|
|
|
@@ -1329,12 +1331,13 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1329
1331
|
if (hint) {
|
|
1330
1332
|
currentPrompt = `${userRequest}\n【上次问题,请修正】${hint}`
|
|
1331
1333
|
}
|
|
1332
|
-
const regen = await runComfyGenerate(currentPrompt, size, { unet })
|
|
1334
|
+
const regen = await runComfyGenerate(currentPrompt, size, { unet, negativePrompt: currentNegativePrompt })
|
|
1333
1335
|
if (!regen.ok) {
|
|
1334
1336
|
logger.warn(`多人校验重试生成失败:${regen.message}`)
|
|
1335
1337
|
break
|
|
1336
1338
|
}
|
|
1337
|
-
currentImages = regen.outputs
|
|
1339
|
+
currentImages = regen.outputs
|
|
1340
|
+
currentNegativePrompt = regen.negativePrompt || currentNegativePrompt
|
|
1338
1341
|
}
|
|
1339
1342
|
|
|
1340
1343
|
// 多候选挑选
|
|
@@ -1345,7 +1348,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1345
1348
|
selectedOutputs = best.outputs
|
|
1346
1349
|
selectedVerdict = best.verdict
|
|
1347
1350
|
if (!multiAccepted && !cfg.multiSendDegradedCandidate) {
|
|
1348
|
-
return { ok: false, discarded: true, message: session.text('.multi-verify-discarded'), verdict: selectedVerdict, outputs: [], prompt: best.prompt }
|
|
1351
|
+
return { ok: false, discarded: true, message: session.text('.multi-verify-discarded'), verdict: selectedVerdict, outputs: [], prompt: best.prompt, negativePrompt: best.negativePrompt }
|
|
1349
1352
|
}
|
|
1350
1353
|
const noteParts = []
|
|
1351
1354
|
if (multiAccepted) {
|
|
@@ -1354,7 +1357,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1354
1357
|
noteParts.push(session.text('.multi-verify-degraded', selectedVerdict.issues.length ? '(' + selectedVerdict.issues.join(';').slice(0, 80) + ')' : ''))
|
|
1355
1358
|
}
|
|
1356
1359
|
if (retries) noteParts.push(session.text('.multi-verify-failed', [selectedVerdict.issues.length ? ':' + selectedVerdict.issues.join(';').slice(0, 80) : '', retries]))
|
|
1357
|
-
return { ok: true, degraded: false, message: noteParts.join('\n'), verdict: selectedVerdict, outputs: selectedOutputs, prompt: best.prompt }
|
|
1360
|
+
return { ok: true, degraded: false, message: noteParts.join('\n'), verdict: selectedVerdict, outputs: selectedOutputs, prompt: best.prompt, negativePrompt: best.negativePrompt }
|
|
1358
1361
|
}
|
|
1359
1362
|
|
|
1360
1363
|
function buildVerifySystemPrompt(multiPerson, planCount) {
|
|
@@ -1367,7 +1370,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1367
1370
|
}
|
|
1368
1371
|
|
|
1369
1372
|
// 共享批量执行器:一次性扣除总价,逐张生成,单张失败只退该张单价。
|
|
1370
|
-
// runOne(i) 需返回 { ok, outputs, seed, prompt, message? };返回数组为多张输出(如视觉校验候选)。
|
|
1373
|
+
// runOne(i) 需返回 { ok, outputs, seed, prompt, negativePrompt, message? };返回数组为多张输出(如视觉校验候选)。
|
|
1371
1374
|
async function executeBatch(USERID, isAdmin, count, unitPrice, runOne) {
|
|
1372
1375
|
const results = []
|
|
1373
1376
|
let successCount = 0
|
|
@@ -1381,7 +1384,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1381
1384
|
const outputs = Array.isArray(item.outputs) ? item.outputs : (item.outputs ? [item.outputs] : [])
|
|
1382
1385
|
if (item.ok && outputs.length) {
|
|
1383
1386
|
successCount += 1
|
|
1384
|
-
results.push({ i, ok: true, outputs, seed: item.seed, prompt: item.prompt || '', note: item.note || '' })
|
|
1387
|
+
results.push({ i, ok: true, outputs, seed: item.seed, prompt: item.prompt || '', negativePrompt: item.negativePrompt || '', note: item.note || '' })
|
|
1385
1388
|
} else {
|
|
1386
1389
|
if (!isAdmin) await refundP(USERID, unitPrice)
|
|
1387
1390
|
if (cfg.outputLogs) logger.warn(`批量第 ${i + 1} 张生成失败(${USERID}):${item.message || '无输出'}`)
|
|
@@ -1477,11 +1480,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1477
1480
|
}
|
|
1478
1481
|
if (!result.ok || !result.outputs || !result.outputs.length) return result
|
|
1479
1482
|
if (!cfg.multiVerifyEnabled) {
|
|
1480
|
-
return { ok: true, outputs: result.outputs, seed: result.seed, prompt: finalPrompt, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
|
|
1483
|
+
return { ok: true, outputs: result.outputs, seed: result.seed, prompt: finalPrompt, negativePrompt: result.negativePrompt, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
|
|
1481
1484
|
}
|
|
1482
|
-
const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet)
|
|
1485
|
+
const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet, result.negativePrompt || multiNegative)
|
|
1483
1486
|
if (!verified.ok) return { ok: false, message: verified.message }
|
|
1484
|
-
return { ok: true, outputs: verified.outputs, seed: result.seed, prompt: verified.prompt || finalPrompt, note: verified.message || '' }
|
|
1487
|
+
return { ok: true, outputs: verified.outputs, seed: result.seed, prompt: verified.prompt || finalPrompt, negativePrompt: verified.negativePrompt || result.negativePrompt || multiNegative, note: verified.message || '' }
|
|
1485
1488
|
}
|
|
1486
1489
|
|
|
1487
1490
|
const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
|
|
@@ -1495,7 +1498,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1495
1498
|
for (const item of results) {
|
|
1496
1499
|
if (item.ok) {
|
|
1497
1500
|
allOutputs.push(...item.outputs)
|
|
1498
|
-
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt })))
|
|
1501
|
+
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
|
|
1499
1502
|
if (item.seed != null) seeds.push(item.seed)
|
|
1500
1503
|
if (item.note) notes.push(item.note)
|
|
1501
1504
|
} else {
|
|
@@ -1669,7 +1672,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1669
1672
|
for (const item of results) {
|
|
1670
1673
|
if (item.ok) {
|
|
1671
1674
|
allOutputs.push(...item.outputs)
|
|
1672
|
-
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || stagePrompts[item.i] || '' })))
|
|
1675
|
+
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || stagePrompts[item.i] || '', negativePrompt: item.negativePrompt })))
|
|
1673
1676
|
if (item.seed != null) seeds.push(item.seed)
|
|
1674
1677
|
} else {
|
|
1675
1678
|
failures.push(`第 ${item.i + 1} 阶段:${item.message}`)
|
|
@@ -2231,10 +2234,12 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2231
2234
|
const pcheck = await precheckPoints(session, USERID, isAdmin, count * cfg.price)
|
|
2232
2235
|
if (!pcheck.ok) return pcheck.message
|
|
2233
2236
|
|
|
2234
|
-
// 原样模式
|
|
2235
|
-
const stripped = stripRawPrefix(text)
|
|
2236
|
-
const raw = stripped.raw
|
|
2237
|
-
const
|
|
2237
|
+
// 原样模式
|
|
2238
|
+
const stripped = stripRawPrefix(text)
|
|
2239
|
+
const raw = stripped.raw
|
|
2240
|
+
const promptSections = splitPositiveNegativePrompt(stripped.prompt)
|
|
2241
|
+
const userPrompt = promptSections.positive
|
|
2242
|
+
const userNegativePrompt = promptSections.negative
|
|
2238
2243
|
if (!userPrompt) return session.text('.no-prompt')
|
|
2239
2244
|
|
|
2240
2245
|
// ComfyUI 就绪
|
|
@@ -2315,9 +2320,15 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2315
2320
|
}
|
|
2316
2321
|
|
|
2317
2322
|
// i2i 运行参数:模式(style/ootd/plain)、--denoise 覆盖值、检测到的能力
|
|
2318
|
-
const i2iRun = i2iImage
|
|
2319
|
-
? { i2iImage, i2i: { mode: (i2iOpts && i2iOpts.mode) || 'plain', denoise: denoise != null ? denoise : null, caps: (i2iOpts && i2iOpts.caps) || null } }
|
|
2320
|
-
: {}
|
|
2323
|
+
const i2iRun = i2iImage
|
|
2324
|
+
? { i2iImage, i2i: { mode: (i2iOpts && i2iOpts.mode) || 'plain', denoise: denoise != null ? denoise : null, caps: (i2iOpts && i2iOpts.caps) || null } }
|
|
2325
|
+
: {}
|
|
2326
|
+
// 用户手写了 negative: 区块时,默认负面词仍保留;用户 tag 只补充未出现的部分。
|
|
2327
|
+
const generationOverrides = Object.assign(
|
|
2328
|
+
{ unet, seed },
|
|
2329
|
+
i2iRun,
|
|
2330
|
+
userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
|
|
2331
|
+
)
|
|
2321
2332
|
|
|
2322
2333
|
// 性能:按张优化(perImageOptimize)时联网搜索只做一次,各图复用同一份结果
|
|
2323
2334
|
const searchCache = perImageOptimize && wantsWebSearch(userPrompt) ? await webSearch(userPrompt) : null
|
|
@@ -2329,7 +2340,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2329
2340
|
const optimized = await optimizePrompt(session, userPrompt, true, i2iRule, searchCache)
|
|
2330
2341
|
p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
|
|
2331
2342
|
}
|
|
2332
|
-
const generated = await runComfyGenerate(p, parsedSize.size,
|
|
2343
|
+
const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
|
|
2333
2344
|
return { ...generated, prompt: p }
|
|
2334
2345
|
}, { USERID, isAdmin, totalPrice: count * cfg.price })
|
|
2335
2346
|
if (!queued.ok) return queued.message
|
|
@@ -2369,7 +2380,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2369
2380
|
if (cfg.queueEnabled) {
|
|
2370
2381
|
try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
2371
2382
|
} else {
|
|
2372
|
-
try { result = await runComfyGenerate(p, parsedSize.size,
|
|
2383
|
+
try { result = await runComfyGenerate(p, parsedSize.size, generationOverrides) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
2373
2384
|
}
|
|
2374
2385
|
if (!result.prompt) result.prompt = p
|
|
2375
2386
|
return result
|
|
@@ -2385,7 +2396,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2385
2396
|
for (const item of results) {
|
|
2386
2397
|
if (item.ok) {
|
|
2387
2398
|
allOutputs.push(...item.outputs)
|
|
2388
|
-
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt })))
|
|
2399
|
+
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
|
|
2389
2400
|
if (item.seed != null) seeds.push(item.seed)
|
|
2390
2401
|
} else {
|
|
2391
2402
|
failures.push(`第 ${item.i + 1} 张:${item.message}`)
|
package/lib/parse.js
CHANGED
|
@@ -186,6 +186,19 @@ function stripRawPrefix(prompt) {
|
|
|
186
186
|
return { raw: false, prompt: text }
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// 将用户手写的正负面区块拆开。只识别独立行标记,避免把普通 tag(如 negative space)误判为负面段。
|
|
190
|
+
const NEGATIVE_SECTION_RE = /(?:^|\r?\n)\s*(?:negative(?:\s*prompt)?|negative_prompt|负面(?:提示词|词)?)\s*[::]\s*/i
|
|
191
|
+
|
|
192
|
+
function splitPositiveNegativePrompt(prompt) {
|
|
193
|
+
const text = String(prompt || '').trim()
|
|
194
|
+
const match = NEGATIVE_SECTION_RE.exec(text)
|
|
195
|
+
if (!match) return { positive: text, negative: '' }
|
|
196
|
+
return {
|
|
197
|
+
positive: text.slice(0, match.index).trim(),
|
|
198
|
+
negative: text.slice(match.index + match[0].length).trim(),
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
189
202
|
function mergeTagText(existing, addition) {
|
|
190
203
|
const tags = []
|
|
191
204
|
const seen = new Set()
|
|
@@ -259,6 +272,7 @@ module.exports = {
|
|
|
259
272
|
parseDenoise,
|
|
260
273
|
RAW_PREFIXES,
|
|
261
274
|
stripRawPrefix,
|
|
275
|
+
splitPositiveNegativePrompt,
|
|
262
276
|
mergeTagText,
|
|
263
277
|
parseNameTags,
|
|
264
278
|
parsePresetList,
|
package/lib/tags.js
CHANGED
|
@@ -236,6 +236,11 @@ function joinPromptParts(parts) {
|
|
|
236
236
|
return tags.join(', ')
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
// 默认负面词始终保留,用户手写负面词只补充未出现的 tag。
|
|
240
|
+
function mergeNegativePrompts(defaultPrompt, userPrompt) {
|
|
241
|
+
return joinPromptParts([defaultPrompt, userPrompt])
|
|
242
|
+
}
|
|
243
|
+
|
|
239
244
|
module.exports = {
|
|
240
245
|
splitTags,
|
|
241
246
|
normalizeTagKey,
|
|
@@ -258,4 +263,5 @@ module.exports = {
|
|
|
258
263
|
isCharacterIdentityTag,
|
|
259
264
|
cleanContentTags,
|
|
260
265
|
joinPromptParts,
|
|
266
|
+
mergeNegativePrompts,
|
|
261
267
|
}
|