koishi-plugin-p-draw 1.2.6 → 1.2.8
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 +72 -16
- package/package.json +1 -1
- package/readme.md +3 -1
package/index.js
CHANGED
|
@@ -191,7 +191,16 @@ exports.Config = Schema.object({
|
|
|
191
191
|
pollInterval: Schema.number().default(2).description('生成状态查询间隔(秒)'),
|
|
192
192
|
|
|
193
193
|
// 模型文件
|
|
194
|
-
unetName: Schema.string().default('
|
|
194
|
+
unetName: Schema.string().default('anima-base-v1.0.safetensors').description('主模型文件名(需与 ComfyUI models/diffusion_models 下的文件名完全一致,含连字符;默认 anima-base-v1.0.safetensors)'),
|
|
195
|
+
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'),
|
|
196
|
+
modelParams: Schema.dict(Schema.object({
|
|
197
|
+
samplerName: Schema.string().description('采样器(如 er_sde / res_2s / dpmpp_2m)'),
|
|
198
|
+
scheduler: Schema.string().description('调度器(如 simple / beta57 / normal)'),
|
|
199
|
+
steps: Schema.number().description('采样步数'),
|
|
200
|
+
cfg: Schema.number().description('CFG 强度'),
|
|
201
|
+
width: Schema.number().description('默认宽度'),
|
|
202
|
+
height: Schema.number().description('默认高度'),
|
|
203
|
+
}).description('该模型的生成参数覆盖')).default({}).description('每个模型独立参数覆盖:key=模型文件名(如 anima-base-v1.0.safetensors、Anima-2.9B-preview-v1.safetensors、anima-aesthetic-v1.1.safetensors),value=要覆盖的参数(samplerName/scheduler/steps/cfg/width/height)。只填需要覆盖的字段,不填的用全局默认。匹配不区分大小写与 -/_。'),
|
|
195
204
|
clipName: Schema.string().default('qwen_3_06b_base.safetensors').description('文本编码器文件名'),
|
|
196
205
|
vaeName: Schema.string().default('qwen_image_vae.safetensors').description('VAE 文件名'),
|
|
197
206
|
|
|
@@ -566,22 +575,28 @@ function animaI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfg
|
|
|
566
575
|
}
|
|
567
576
|
}
|
|
568
577
|
|
|
569
|
-
// 「换风格(漫画化)」i2i 工作流:LoadImage → Canny
|
|
578
|
+
// 「换风格(漫画化)」i2i 工作流:LoadImage → 预处理器(LineArt / Canny)→ AnimaLLLiteApply_sdscripts 锁构图,
|
|
570
579
|
// VAEEncode 原图作起点,KSampler 用较高 denoise 整体转二次元风格。
|
|
571
580
|
// 注意:Anima 是 MiniTrainDIT 架构(隐藏层 3584 维),与 Qwen-Image 系 ControlNet 不兼容
|
|
572
581
|
// (InstantX 控制网期望 3584 维文本嵌入,Anima 只给 1024 维 → LayerNorm 崩溃)。
|
|
573
582
|
// 因此这里使用 Anima 原生支持的 ControlNet-LLLite(kohya-ss/ComfyUI-Anima-LLLite),
|
|
574
583
|
// 节点直接对 MODEL 打补丁并返回补丁后的模型,交给 KSampler。
|
|
575
|
-
|
|
584
|
+
// 预处理器:comfyui_controlnet_aux 的 AnimeLineArt/LineArt 节点(需安装该自定义节点);
|
|
585
|
+
// 未安装时回退 ComfyUI 内置 Canny。anima-lllite-lineart 权重是按 lineart 训练的,用 LineArt 比 Canny 更贴。
|
|
586
|
+
function animaStyleI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, controlNetModel, controlNetStrength, denoise, preprocessor) {
|
|
587
|
+
const detectRes = Math.max(width, height)
|
|
588
|
+
const preprocessorNode = preprocessor === 'AnimeLineArtPreprocessor' || preprocessor === 'LineArtPreprocessor'
|
|
589
|
+
? { class_type: preprocessor, inputs: { image: ['13', 0], detect_resolution: detectRes, resolution: detectRes } }
|
|
590
|
+
: { class_type: 'Canny', inputs: { image: ['13', 0], low_threshold: 0.4, high_threshold: 0.8 } }
|
|
576
591
|
return {
|
|
577
|
-
'44': { class_type: 'UNETLoader', inputs: { unet_name: cfg.unetName, weight_dtype: 'fp8_e4m3fn' } },
|
|
592
|
+
'44': { class_type: 'UNETLoader', inputs: { unet_name: cfg.i2iUnetName || cfg.unetName, weight_dtype: 'fp8_e4m3fn' } },
|
|
578
593
|
'45': { class_type: 'CLIPLoader', inputs: { clip_name: cfg.clipName, type: 'stable_diffusion', device: 'default' } },
|
|
579
594
|
'15': { class_type: 'VAELoader', inputs: { vae_name: cfg.vaeName } },
|
|
580
595
|
'13': { class_type: 'LoadImage', inputs: { image: inputImage } },
|
|
581
596
|
'30': { class_type: 'VAEEncode', inputs: { pixels: ['13', 0], vae: ['15', 0] } },
|
|
582
597
|
'11': { class_type: 'CLIPTextEncode', inputs: { text: prompt, clip: ['45', 0] } },
|
|
583
598
|
'12': { class_type: 'CLIPTextEncode', inputs: { text: negativePrompt, clip: ['45', 0] } },
|
|
584
|
-
'70':
|
|
599
|
+
'70': preprocessorNode,
|
|
585
600
|
'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 } },
|
|
586
601
|
'19': {
|
|
587
602
|
class_type: 'KSampler',
|
|
@@ -649,7 +664,7 @@ function buildI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfg
|
|
|
649
664
|
return {
|
|
650
665
|
kind: 'controlnet',
|
|
651
666
|
denoise,
|
|
652
|
-
promptBody: animaStyleI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, caps.controlNet.model, Number(cfg.i2iControlNetStrength) || 0.7, denoise),
|
|
667
|
+
promptBody: animaStyleI2IWorkflow(cfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, inputImage, caps.controlNet.model, Number(cfg.i2iControlNetStrength) || 0.7, denoise, caps.controlNet.preprocessor),
|
|
653
668
|
}
|
|
654
669
|
}
|
|
655
670
|
// 没有 ControlNet:换风格又不想崩构图,denoise 自动压到 0.5 以内
|
|
@@ -1431,10 +1446,12 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1431
1446
|
const vaeList = availableModels(objectInfo, 'VAELoader', 'vae_name')
|
|
1432
1447
|
payload.unet_available = unetList.includes(cfg.unetName)
|
|
1433
1448
|
payload.unet_models = unetList
|
|
1449
|
+
payload.i2i_unet_available = unetList.includes(String(cfg.i2iUnetName || '').trim() || cfg.unetName)
|
|
1434
1450
|
payload.clip_available = clipList.includes(cfg.clipName)
|
|
1435
1451
|
payload.vae_available = vaeList.includes(cfg.vaeName)
|
|
1436
1452
|
} else {
|
|
1437
1453
|
payload.unet_available = undefined
|
|
1454
|
+
payload.i2i_unet_available = undefined
|
|
1438
1455
|
payload.clip_available = undefined
|
|
1439
1456
|
payload.vae_available = undefined
|
|
1440
1457
|
}
|
|
@@ -1480,6 +1497,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1480
1497
|
`版本:${payload.comfyui_version || '未知'}`,
|
|
1481
1498
|
`GPU:${payload.gpu || '未知'}(显存 ${payload.vram_total_mb}MB / 空闲 ${payload.vram_free_mb}MB)`,
|
|
1482
1499
|
`主模型:${cfg.unetName} ${modelStatus(cfg.unetName, payload.unet_available)}`,
|
|
1500
|
+
`换风格模型:${cfg.i2iUnetName || cfg.unetName} ${modelStatus(cfg.i2iUnetName || cfg.unetName, payload.i2i_unet_available)}`,
|
|
1483
1501
|
`文本编码器:${cfg.clipName} ${modelStatus(cfg.clipName, payload.clip_available)}`,
|
|
1484
1502
|
`VAE:${cfg.vaeName} ${modelStatus(cfg.vaeName, payload.vae_available)}`,
|
|
1485
1503
|
`可用尺寸:${payload.allowed_sizes.join('、')}`,
|
|
@@ -1590,6 +1608,26 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1590
1608
|
return String(chosen).trim()
|
|
1591
1609
|
}
|
|
1592
1610
|
|
|
1611
|
+
// 读取某模型在 modelParams 中的独立覆盖参数(只允许采样相关字段,避免误改 unet/clip/vae 等路径)
|
|
1612
|
+
const MODEL_OVERRIDE_KEYS = ['samplerName', 'scheduler', 'steps', 'cfg', 'width', 'height']
|
|
1613
|
+
function resolveModelParams(name) {
|
|
1614
|
+
if (!name || !cfg.modelParams || typeof cfg.modelParams !== 'object') return {}
|
|
1615
|
+
const norm = (s) => String(s || '').trim().toLowerCase().replace(/[-_]/g, '~')
|
|
1616
|
+
const key = norm(name)
|
|
1617
|
+
if (!key) return {}
|
|
1618
|
+
const normalized = {}
|
|
1619
|
+
for (const [k, v] of Object.entries(cfg.modelParams)) {
|
|
1620
|
+
normalized[norm(k)] = v
|
|
1621
|
+
}
|
|
1622
|
+
const raw = normalized[key]
|
|
1623
|
+
if (!raw || typeof raw !== 'object') return {}
|
|
1624
|
+
const out = {}
|
|
1625
|
+
for (const k of MODEL_OVERRIDE_KEYS) {
|
|
1626
|
+
if (raw[k] !== undefined && raw[k] !== null && raw[k] !== '') out[k] = raw[k]
|
|
1627
|
+
}
|
|
1628
|
+
return out
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1593
1631
|
// 名称匹配:精确 > 前缀唯一 > 包含唯一;多个匹配返回 { multiple: [...] }
|
|
1594
1632
|
function matchUnetModel(input, list) {
|
|
1595
1633
|
// 规范化:连字符与下划线视为等价(用户常写 anima-aesthetic,模型名是 anima_aesthetic)
|
|
@@ -1611,15 +1649,16 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1611
1649
|
const sizes = parseAllowedSizes()
|
|
1612
1650
|
const requestedWidth = (size && size[0]) || overrides.width || cfg.width
|
|
1613
1651
|
const requestedHeight = (size && size[1]) || overrides.height || cfg.height
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
const
|
|
1617
|
-
const
|
|
1652
|
+
const unetName = overrides.unet || cfg.unetName
|
|
1653
|
+
// 应用该模型的独立参数覆盖(modelParams),再叠加命令级 overrides(overrides.steps/cfg 优先于模型级)
|
|
1654
|
+
const modelSpecific = resolveModelParams(unetName)
|
|
1655
|
+
const workCfg = Object.assign({}, cfg, modelSpecific, { unetName })
|
|
1656
|
+
const width = Math.round(Number(requestedWidth) || workCfg.width)
|
|
1657
|
+
const height = Math.round(Number(requestedHeight) || workCfg.height)
|
|
1658
|
+
const steps = Math.round(Number(overrides.steps) || workCfg.steps)
|
|
1659
|
+
const cfgVal = Number(overrides.cfg) || workCfg.cfg
|
|
1618
1660
|
const seed = Number(overrides.seed) || crypto.randomInt(1, 2 ** 32 - 1)
|
|
1619
1661
|
const negativePrompt = overrides.negativePrompt || cfg.negativePrompt || ''
|
|
1620
|
-
const unetName = overrides.unet || cfg.unetName
|
|
1621
|
-
|
|
1622
|
-
const workCfg = unetName && unetName !== cfg.unetName ? Object.assign({}, cfg, { unetName }) : cfg
|
|
1623
1662
|
const i2iImage = overrides.i2iImage
|
|
1624
1663
|
if (i2iImage && cfg.customWorkflowEnabled && cfg.customWorkflowPath) {
|
|
1625
1664
|
return { ok: false, message: 'i2i(以图生图)暂不支持自定义工作流(customWorkflowEnabled),请关闭后再试。' }
|
|
@@ -3187,11 +3226,12 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
3187
3226
|
return data.name
|
|
3188
3227
|
}
|
|
3189
3228
|
|
|
3190
|
-
// 检测 i2i 可用能力:Anima ControlNet-LLLite 节点与权重(comfyui-anima-lllite / kohya-ss/ComfyUI-Anima-LLLite
|
|
3191
|
-
//
|
|
3229
|
+
// 检测 i2i 可用能力:Anima ControlNet-LLLite 节点与权重(comfyui-anima-lllite / kohya-ss/ComfyUI-Anima-LLLite)、
|
|
3230
|
+
// LineArt 预处理器(comfyui_controlnet_aux,可选)与 Anima IP-Adapter 专用节点(comfyui-anima-ipadapter)。
|
|
3231
|
+
// 检测失败全部视为不可用(回退普通 img2img)。
|
|
3192
3232
|
// 注意:Qwen-Image InstantX ControlNet 与 Anima(MiniTrainDIT,3584 维)架构不兼容,这里只认 LLLite 权重。
|
|
3193
3233
|
async function detectI2ICapabilities() {
|
|
3194
|
-
const caps = { controlNet: { available: false, model: null }, ipAdapter: { available: false } }
|
|
3234
|
+
const caps = { controlNet: { available: false, model: null, preprocessor: 'Canny' }, ipAdapter: { available: false } }
|
|
3195
3235
|
let info
|
|
3196
3236
|
try {
|
|
3197
3237
|
info = await getObjectInfoCached()
|
|
@@ -3199,9 +3239,16 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
3199
3239
|
return caps
|
|
3200
3240
|
}
|
|
3201
3241
|
if (!info || typeof info !== 'object') return caps
|
|
3242
|
+
// LineArt 预处理器(comfyui_controlnet_aux):AnimeLineArt 更贴近漫画线条,优先;LineArt 次之;都没有则回退 Canny
|
|
3243
|
+
if (info.AnimeLineArtPreprocessor) {
|
|
3244
|
+
caps.controlNet.preprocessor = 'AnimeLineArtPreprocessor'
|
|
3245
|
+
} else if (info.LineArtPreprocessor) {
|
|
3246
|
+
caps.controlNet.preprocessor = 'LineArtPreprocessor'
|
|
3247
|
+
}
|
|
3202
3248
|
// Anima ControlNet-LLLite:lllite_name 从 models/controlnet 目录读取,只挑 anima-lllite 系权重
|
|
3203
3249
|
if (info.AnimaLLLiteApply_sdscripts) {
|
|
3204
3250
|
const llliteModels = availableModels(info, 'AnimaLLLiteApply_sdscripts', 'lllite_name')
|
|
3251
|
+
if (cfg.outputLogs) logger.info(`[p-draw] i2i LLLite 节点存在,models/controlnet 文件列表=${JSON.stringify(llliteModels)}`)
|
|
3205
3252
|
if (llliteModels.length) {
|
|
3206
3253
|
const configured = String(cfg.controlNetModel || '').trim()
|
|
3207
3254
|
const norm = (s) => String(s).toLowerCase().replace(/[-_ ]/g, '')
|
|
@@ -3213,6 +3260,15 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
3213
3260
|
caps.controlNet.model = configured && llliteModels.includes(configured) ? configured : best
|
|
3214
3261
|
caps.controlNet.available = !!caps.controlNet.model
|
|
3215
3262
|
}
|
|
3263
|
+
// 换风格专用主模型必须是 28-block(与 LLLite 权重匹配),检测它是否已安装
|
|
3264
|
+
const styleModel = String(cfg.i2iUnetName || '').trim() || cfg.unetName
|
|
3265
|
+
const unetList = availableModels(info, 'UNETLoader', 'unet_name')
|
|
3266
|
+
if (unetList.length && !unetList.includes(styleModel)) {
|
|
3267
|
+
logger.warn(`[p-draw] i2i 换风格专用主模型 ${styleModel} 不在 models/diffusion_models 中(现有:${JSON.stringify(unetList)})。LLLite 权重为 28-block,请安装 anima-base-v1.0 或修改 i2iUnetName 配置,否则换风格会报 depth_embed slices missing`)
|
|
3268
|
+
caps.controlNet.modelMismatch = true
|
|
3269
|
+
}
|
|
3270
|
+
} else if (cfg.outputLogs) {
|
|
3271
|
+
logger.info('[p-draw] i2i 未检测到 AnimaLLLiteApply_sdscripts 节点(请确认已安装 kohya-ss/ComfyUI-Anima-LLLite 并重启 ComfyUI)')
|
|
3216
3272
|
}
|
|
3217
3273
|
if (info.AnimaIPAdapterLoader && info.AnimaIPAdapterApply && info.AnimaSiglipeEncodeImage) {
|
|
3218
3274
|
caps.ipAdapter.available = true
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -67,6 +67,8 @@ plugins:
|
|
|
67
67
|
|----|------|
|
|
68
68
|
| `comfyuiBaseUrl` | ComfyUI API 地址(默认 `http://127.0.0.1:8188`) |
|
|
69
69
|
| `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
|
+
| `modelParams` | 每个模型独立参数覆盖(字典):`key`=模型文件名,`value`=要覆盖的 `samplerName` / `scheduler` / `steps` / `cfg` / `width` / `height`。让不同模型用各自最佳采样参数,只填需要覆盖的字段,命令级 `--steps`/`--cfg` 仍优先 |
|
|
70
72
|
| `customWorkflowEnabled` / `customWorkflowPath` | 使用自定义工作流 JSON(相对插件目录) |
|
|
71
73
|
| `width` / `height` / `allowedSizes` | 默认尺寸与可用尺寸列表 |
|
|
72
74
|
| `steps` / `cfg` / `samplerName` / `scheduler` | 采样参数 |
|
|
@@ -111,5 +113,5 @@ plugins:
|
|
|
111
113
|
- 券为一次性、**按张数消耗**:`x3` 需 3 张,每张图独立做一次 LLM 优化;券不足时该批不优化也不扣券(提示 `token-short`)。等待回复时间由 `couponAskTimeout`(秒)控制。
|
|
112
114
|
- **管理员在全局关闭时自动免费优化**(不耗券);`无优化` 原样模式不消耗、不询问。平台不支持交互确认时退回自动消耗逻辑。
|
|
113
115
|
- **未使用优化的提示**:单人生图没走 LLM 优化时会明确提示原因——未配置 LLM、未消耗优化券、或 `无优化` 原样模式;多人指令强依赖 LLM 规划,未配置 `llmBaseUrl`/`llmModel` 时会直接提示「多人指令需要 LLM 规划」。
|
|
114
|
-
- **i2i 增强模式依赖的节点**:换风格(漫画化)需要 ComfyUI 安装 [kohya-ss/ComfyUI-Anima-LLLite](https://github.com/kohya-ss/ComfyUI-Anima-LLLite) 自定义节点,并在 `models/controlnet/` 放一个 `anima-lllite` 系权重(`AnimaLLLiteApply_sdscripts`
|
|
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 并提示,不影响出图。
|
|
115
117
|
- 本地模型不一定认识新角色,可用 `添加角色` 手动补充角色 tags。
|