概述
一个 ComfyUI 自定义节点(SparkCanvas),用自然语言驱动 Stable Diffusion 生图。输入一句话,自动规划 pipeline、生成 tags、选择 ControlNet、局部重绘——不需要手动连线。核心设计是”LLM 做决策,代码做组装”:LLM 输出轻量 JSON 参数,Python 代码走预定义分支调用 ComfyUI API。
关键要点
- 一句话出图:用户输入自然语言,LLM(Qwen3.6-VL)自动规划生成模式、翻译成 danbooru tags,代码组装 ComfyUI 节点链路完成生图
- 五种生成模式:txt2img / img2img / controlnet_pose / character_swap / inpaint,LLM 根据 prompt 和参考图自动选择
- 多模态视觉理解:Qwen3.6-VL 直接”看”参考图内容做判断(人物 vs 非人类角色、构图方向、衣着等)
- AnimeSeg 自动分割:inpaint 模式用 DINOv2 做 13 类二次元语义分割,自动生成 mask,无需手动涂抹
- LLM 做决策而非执行:不让 LLM 生成完整 workflow JSON,只做”选择题”和”填空题”,7B 本地模型即可胜任
技术细节
技术栈
| 组件 | 选型 | 说明 |
|---|---|---|
| 图片生成 | Pony Diffusion V6 XL (SDXL) | 二次元专精模型 |
| 工作流引擎 | ComfyUI 0.19.3 | 节点式 SD 前端 |
| LLM 后端 | vLLM + Qwen3.6-VL | 本地部署,多模态视觉语言模型,负责 pipeline 规划和 tag 生成 |
| ControlNet | depth / openpose / animalpose | 姿势和构图控制 |
| IP-Adapter | Plus (SDXL) | 角色风格迁移 |
| 语义分割 | AnimeSeg DINOv2 | 二次元专用 13 类分割 |
| 硬件 | 单卡 GPU (VRAM 12GB+) | 推理用 fp16 |
架构设计
用户输入 (自然语言 prompt + 可选参考图)
│
▼
┌─────────────────────────────────┐
│ Step 1: LLM Pipeline Planner │ Qwen3.6-VL → JSON
│ 判断模式 / 比例 / denoise 等 │ {mode, ratio, denoise, ...}
│ (多模态:可以看参考图内容) │
└──────────────┬──────────────────┘
│
▼
┌─────────────────────────────────┐
│ Step 2: LLM Tag Generator │ 自然语言 → danbooru tags
│ "白猫娘月光下" → "1girl, │
│ cat_ears, white_hair, moon..." │
└──────────────┬──────────────────┘
│
▼
┌─────────────────────────────────┐
│ Step 3: Load Checkpoint + CLIP │ Pony V6 XL
│ Step 4: Encode Prompt (tags) │ positive + negative
│ Step 5: Prepare Latent │ 根据 mode 分支处理
│ Step 6: KSampler │ euler_ancestral, 25 steps
│ Step 7: VAE Decode │ latent → image
└──────────────┬──────────────────┘
│
▼
输出图片 + 调试信息 + workflow JSON
五种生成模式
LLM 根据用户 prompt 和参考图自动选择:
| 模式 | 触发条件 | 技术实现 |
|---|---|---|
| txt2img | 无参考图,或图片与需求无关 | 空 latent → KSampler |
| img2img | “重画/修改/改成” + 参考图 | VAE Encode → KSampler (denoise 0.5-0.8) |
| controlnet_pose | “保持姿势/用这个动作” | 预处理器提取骨骼 → ControlNet 引导 |
| character_swap | 两张图 + “替换角色” | IP-Adapter 风格迁移 + ControlNet 姿势 |
| inpaint | “换衣服/换发型/改头发” | AnimeSeg 自动 mask → noise_mask 局部重绘 |
搭建流程
1. 环境准备
# ComfyUI
cd ~/sd-workspace
git clone https://github.com/comfyanonymous/ComfyUI.git comfyui
cd comfyui
pip install -r requirements.txt
# vLLM (LLM 推理服务)
pip install vllm
vllm serve Qwen/Qwen3.6-VL --port 11434 --gpu-memory-utilization 0.3
2. 模型下载
# SDXL 模型 (Pony V6 XL)
# → models/checkpoints/pony-v6-xl.safetensors
# ControlNet 模型
# → models/controlnet/
# - diffusion_pytorch_model.fp16.safetensors (depth)
# - control-lora-openposeXL2-rank256.safetensors (openpose)
# IP-Adapter
# → models/ipadapter/ip-adapter-plus_sdxl_vit-h.safetensors
# → models/clip_vision/CLIP-ViT-H-14-laion2B-s32B-b79K.safetensors
3. 自定义节点安装
cd custom_nodes/
# ControlNet 预处理器 (depth/openpose/animalpose)
git clone https://github.com/Fannovel16/comfyui_controlnet_aux
# IP-Adapter
git clone https://github.com/cubiq/ComfyUI_IPAdapter_plus
# Impact Pack (CLIPSeg 等辅助节点)
git clone https://github.com/ltdrdata/ComfyUI-Impact-Pack
cd ComfyUI-Impact-Pack && pip install -r requirements.txt && python install.py
# AnimeSeg (二次元语义分割)
pip install anime-seg
4. SparkCanvas 节点
mkdir -p custom_nodes/spark-canvas
# 将 __init__.py 放入该目录
核心代码约 1000 行,包含:
_plan_pipeline()— LLM 规划生成模式(多模态,可看图判断)_generate_tags()— LLM 生成 danbooru tags_generate_mask_animeseg()— AnimeSeg 自动分割生成 mask_apply_controlnet()— ControlNet 应用_build_equivalent_workflow()— 导出可复现的 workflow JSONSparkCanvas.generate()— 主生成函数
关键技术实现
LLM Pipeline Planner(多模态视觉理解)
用 Qwen3.6-VL 做 pipeline 规划。这是一个多模态视觉语言模型,可以直接”看”参考图内容来做判断。输入用户 prompt + 参考图(base64),输出 JSON:
{
"mode": "inpaint",
"ratio": "auto",
"denoise": 0.85,
"inpaint_target": "clothing",
"description": "A wolf girl wearing a red dress, standing pose...",
"reason": "User wants to change clothing, which is a specific part modification"
}
Prompt 设计要点:
- 明确列出每种 mode 的触发关键词(中英文都要覆盖)
- ratio 规则:用户提到”竖图/壁纸”时 LLM 主动选比例,否则跟随参考图
- 多模态优势:LLM 直接看参考图判断是人物还是非人类角色(影响 ControlNet 选择)
Tag 生成策略
不直接用自然语言做 prompt,而是让 LLM 翻译成 danbooru tag 格式:
输入: "一只白色猫娘坐在窗台上"
输出: "score_9, score_8_up, 1girl, cat_ears, white_hair, sitting, windowsill,
indoor, sunlight, detailed_background, masterpiece"
Pony V6 XL 对 danbooru tags 的响应远好于自然语言描述。score_9, score_8_up 是 Pony 特有的质量引导 tag。
ControlNet 类型自动选择
三种预处理器对应不同场景:
| 类型 | 预处理器 | 适用场景 | 特点 |
|---|---|---|---|
| depth | DepthAnythingV2 | 保持构图/背景 | 锁定轮廓,新角色被迫适配旧体型 |
| openpose | DWOpenpose | 人物姿势 | 只提取骨骼,体型自由 |
| animalpose | AnimalPose | 非人类二次元角色 | 动物骨骼关键点 |
LLM 根据参考图内容自动选择:看到非人类角色 → animalpose,人物 → openpose,场景 → depth。
AnimeSeg 二次元语义分割
inpaint 模式的核心。用 AnimeSeg DINOv2 模型做 13 类语义分割:
| Class ID | 类别 | 用途 |
|---|---|---|
| 0 | background | 换背景 |
| 1 | skin | 皮肤修改 |
| 2 | face | 换脸 |
| 3 | hair_main | 换发型/发色 |
| 10 | clothes | 换衣服 |
| 4-5 | eyes | 换瞳色 |
| 6-9 | eyebrow/nose/mouth | 面部细节 |
工作流程:
用户说"换衣服" → LLM 提取 inpaint_target="clothing"
→ AnimeSeg 分割出 class 10 (clothes) 区域
→ 膨胀 mask 10px 覆盖边缘
→ VAE Encode + noise_mask → KSampler 只重绘衣服区域
实测效果:角色的衣服区域覆盖率 ~7-10%,分割边界清晰,重绘后脸部和背景完全不变。
分辨率系统 (ratio + size)
模仿 NanoBanana 的设计,用 ratio × size 组合替代固定分辨率:
ratio: auto / 1:1 / 2:3 / 3:2 / 3:4 / 4:3 / 9:16 / 16:9
size: 1K (1024px) / 1.5K (1536px) / 2K (2048px)
SDXL 原生训练分辨率约 1MP(1024×1024),超出会导致画面撕裂。1.5K/2K 模式先在 1K 生成再放大。
auto 模式优先级:用户手动选 > LLM 判断 > 参考图比例 > 默认 1:1。
如何让 LLM 输出正确的工作流参数
整个系统最关键也最脆弱的环节。一个 7B 参数的本地模型要准确理解用户意图、选对模式、填对参数,需要在 prompt 工程上下功夫。
Prompt 设计原则
1. 结构化输出约束
不让 LLM 自由发挥,严格限定输出格式:
Output ONLY a JSON object with these fields:
- "mode": one of "txt2img", "img2img", "controlnet_pose", "character_swap", "inpaint"
- "ratio": one of "auto", "1:1", "2:3", ...
- "denoise": float 0.3-1.0
...
Output valid JSON only, no markdown.
每个字段都给出枚举值或取值范围,LLM 不需要”创造”答案,只需从有限选项中选择。大幅降低输出错误概率。
2. 中英文关键词双覆盖
用户可能用中文也可能用英文描述需求,prompt 里同时列出两种触发词:
- "img2img": 关键词: 重画, 修改, 改成, 换背景, 变成
- "controlnet_pose": 关键词: 保持姿势, 用这个姿势, 同样的动作
- "inpaint": 关键词: 换衣服, 换发型, 改头发颜色, 局部修改
不管用户说”换件衣服”还是”change the outfit”,LLM 都能匹配到正确的模式。
3. 参数联动规则
不同模式需要不同的参数组合,prompt 里明确写出联动关系:
换衣服/换装 → denoise 0.65-0.75 (change clothing style but keep body)
换发型/改头发颜色 → denoise 0.6-0.7
换背景 → denoise 0.8-0.95 (background can change freely)
denoise 值直接影响重绘强度——衣服换装不需要太高(保留身体),换背景可以很高(背景可以完全变)。这些经验值是反复测试调出来的。
4. 多模态视觉辅助判断
Qwen3.6-VL 是多模态模型,可以直接看参考图。代码里把参考图缩放到 512px、转成 JPEG base64 传给 LLM:
# 缩略图避免 token 过多
img.thumbnail((512, 512))
img.save(buf, format="JPEG", quality=80)
b64 = base64.b64encode(buf.getvalue()).decode()
# 多模态消息格式
messages.append({"role": "user", "content": [
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
{"type": "text", "text": user_content}
]})
这让 LLM 能做出更准确的判断:
- 看到二次元角色 → 选 animalpose 而不是 openpose
- 看到全身照 → inpaint 时知道衣服在哪
- 看到风景图 → 判断是 img2img 而不是 inpaint
5. 兜底与容错
LLM 输出不一定总是合法 JSON,代码里做了多层容错:
try:
plan = json.loads(text)
except:
# JSON 解析失败 → 回退到最安全的 txt2img
plan = {"mode": "txt2img", "denoise": 1.0, ...}
# 字段缺失 → 填默认值
plan.setdefault("mode", "txt2img")
plan.setdefault("denoise", 1.0 if plan["mode"] == "txt2img" else 0.65)
# 有图但 LLM 选了 txt2img 以外的模式 → 强制修正
if not has_image and plan["mode"] != "txt2img":
plan["mode"] = "txt2img"
Tag 生成的 Prompt 工程
Tag 生成用的是 Completions API(不是 Chat),因为 Pony V6 XL 需要的是 danbooru 风格的逗号分隔 tag,不是自然语言:
prompt = (
"Convert this description to Pony V6 XL tags. "
"Output ONLY comma-separated tags starting with score_9, score_8_up, score_7_up. "
"Include detailed tags for subject, appearance, pose, background, lighting.nn"
f"Description: {description}nnTags:"
)
关键技巧:
- 强制以
score_9, score_8_up, score_7_up,开头(Pony 质量引导 tag) - 代码里硬编码前缀,不依赖 LLM 记住:
return "score_9, score_8_up, score_7_up," + resp["choices"][0]["text"] - 用 Completions 而不是 Chat,避免 LLM 加多余的解释文字
参数分发与分支调度
LLM 输出 JSON 后,SparkCanvas 按以下逻辑分发参数:
plan = _plan_pipeline(prompt, has_image, image_b64)
│
├─ ratio 计算(三级优先级)
│ ├─ 用户手动选了具体比例 → 直接用
│ ├─ auto + LLM 返回了具体比例 → 用 LLM 的判断
│ ├─ auto + LLM 也返回 auto + 有参考图 → 匹配参考图比例
│ └─ auto + 无参考图 → 默认 1:1
│
├─ mode 分支
│ ├─ txt2img → 空 latent (w×h)
│ ├─ img2img → VAE Encode 参考图
│ ├─ controlnet_pose → 预处理器提取骨骼 + ControlNet
│ ├─ character_swap → IP-Adapter + ControlNet
│ └─ inpaint
│ ├─ 有手动 mask → 直接用
│ ├─ 有 inpaint_target → AnimeSeg 自动生成 mask
│ │ └─ target 映射: "clothing"→[10], "hair"→[3,11], "face"→[2]...
│ └─ 都没有 → 回退到 img2img (高 denoise)
│
└─ KSampler (统一采样)
├─ 普通路径: prepare_noise(latent) → sample(latent)
└─ inpaint 路径: prepare_noise(samples) → sample(samples, noise_mask=mask)
关于 Comfy-Cozy Agent
当前版本的 SparkCanvas 没有使用 Comfy-Cozy Agent。
Comfy-Cozy Agent 是一个让 LLM 动态生成和执行 ComfyUI workflow 的框架——LLM 直接输出节点连接关系,理论上可以组合任意工作流。SparkCanvas 选择了不同的路线:LLM 只做决策,不做执行。 LLM 输出的是高层参数(mode、denoise、ratio 等),具体的节点调用、模型加载、tensor 操作都由 Python 代码硬编码。
| Comfy-Cozy Agent 路线 | SparkCanvas 路线 | |
|---|---|---|
| 灵活性 | 高,LLM 可以组合任意节点 | 低,只有预定义的 5 种模式 |
| 可靠性 | 低,LLM 可能生成无效连接 | 高,代码路径经过测试 |
| LLM 要求 | 需要理解 ComfyUI 节点 API | 只需要理解用户意图 |
| 延迟 | 高,LLM 需要生成完整 workflow | 低,LLM 只输出几个参数 |
| 本地 7B 模型 | 很难可靠工作 | 足够胜任 |
对于本地 7B 模型来说,让它理解”用户想换衣服”比让它正确连接 VAEEncode → SetLatentNoiseMask → KSampler 容易得多。
LLM 如何驱动 ComfyUI 节点
SparkCanvas 最核心的设计——用 LLM 的语义理解能力替代人工连线。整个过程分为两次 LLM 调用和一次参数组装。
第一次 LLM 调用:Pipeline Planner(决策层)
用户输入一句自然语言,LLM 需要做出一系列决策,直接决定后续调用哪些 ComfyUI 节点、传什么参数。
输入给 LLM 的信息:
- 用户 prompt(原文)
- 是否有参考图(bool)
- 参考图缩略图(base64,缩放到 512px 以内,降低 token 消耗)
LLM 需要输出的决策:
{
"mode": "inpaint",
"ratio": "2:3",
"denoise": 0.85,
"controlnet_type": "animalpose",
"controlnet_strength": 0.8,
"inpaint_target": "clothing",
"ipadapter_weight": 0.75,
"description": "...",
"reason": "..."
}
Prompt 工程的关键设计:
- 每种 mode 的触发条件用中英文双语列出,避免 LLM 对中文指令理解偏差:
- txt2img: 用户想从零生成 / “画一个” / “生成” / no reference needed
- inpaint: 用户想修改图片的特定部分 / “换衣服” / “改头发” / “change clothing”
- ratio 规则需要明确”什么时候不跟原图”:
- 用户说”调整比例/换画幅/自由发挥” → LLM 自己选比例
- 用户说”竖图/手机壁纸” → “2:3” 或 “9:16”
- 没提到比例 → “auto”(系统层面跟随原图)
- 多模态视觉理解的实际作用——LLM 看到参考图后能判断:
- 图里是人类还是非人类角色 → 影响 controlnet_type 选择
- 图里角色穿了什么 → 影响 inpaint 时 description 的准确性
- 图的构图是横版还是竖版 → 影响 ratio 建议
第二次 LLM 调用:Tag Generator(翻译层)
Pipeline Planner 输出的 description 是英文自然语言,但 Pony V6 XL 对 danbooru tag 格式的响应远好于自然语言。所以需要第二次 LLM 调用做”翻译”。
输入: Planner 输出的 description(英文自然语言描述)
输出: danbooru 风格的 tag 序列
输入: "A white cat girl sitting on a windowsill under moonlight"
输出: "score_9, score_8_up, score_7_up, 1girl, solo, cat_ears, cat_tail,
white_hair, long_hair, sitting, windowsill, night, moonlight,
indoor, window, detailed_background, masterpiece"
Tag 生成的 Prompt 设计要点:
- 强制以
score_9, score_8_up开头(Pony 质量引导) - 要求输出纯 tag 序列,不要自然语言
- 提示 LLM 加入构图 tag(如
from_above,close-up)和氛围 tag(如dramatic_lighting)
参数组装:LLM 输出 → ComfyUI 节点参数
最关键的”胶水层”——把 LLM 的 JSON 决策翻译成具体的 ComfyUI API 调用。
决策到节点的映射关系:
LLM 决策字段 → 影响的 ComfyUI 节点/参数
─────────────────────────────────────────────────────
mode → 决定整条节点链路(见下方分支图)
ratio + size → EmptyLatentImage 的 width/height
或 img2img 时 image resize 的目标尺寸
denoise → KSampler.denoise
controlnet_type → 选择哪个预处理器节点 + 加载哪个 ControlNet 模型
controlnet_strength → ControlNetApplyAdvanced.strength
inpaint_target → AnimeSeg 的 class ID 选择 → mask 生成
ipadapter_weight → IPAdapterAdvanced.weight
description → tags → CLIPTextEncode 的 text 输入
五种模式的节点链路:
txt2img:
CheckpointLoader → CLIPTextEncode(+/-) → EmptyLatentImage
→ KSampler(denoise=1.0) → VAEDecode
img2img:
CheckpointLoader → CLIPTextEncode(+/-) → LoadImage → VAEEncode
→ KSampler(denoise=0.5~0.8) → VAEDecode
controlnet_pose:
CheckpointLoader → CLIPTextEncode(+/-) → LoadImage
→ Preprocessor(depth/openpose/animalpose) → ControlNetLoader
→ ControlNetApplyAdvanced(strength) → EmptyLatentImage
→ KSampler → VAEDecode
character_swap:
CheckpointLoader → CLIPTextEncode(+/-) → LoadImage(场景图)
→ Preprocessor → ControlNetApplyAdvanced
→ LoadImage(角色参考图) → CLIPVisionLoader → IPAdapterAdvanced(weight)
→ KSampler(model=IPAdapter修改后的model) → VAEDecode
inpaint:
CheckpointLoader → CLIPTextEncode(+/-) → LoadImage
→ AnimeSeg(target) → GenerateMask → VAEEncode + noise_mask
→ KSampler(denoise=0.85, noise_mask) → VAEDecode
代码层面的分支处理(简化版):
# Step 1: LLM 决策
plan = _plan_pipeline(prompt, has_image, image_b64)
mode = plan["mode"]
# Step 2: LLM 生成 tags
tags = _generate_tags(plan["description"])
# Step 3: 加载模型(所有模式共用)
model, clip, vae = CheckpointLoaderSimple.load(ckpt_name)
pos = CLIPTextEncode.encode(clip, tags)
neg = CLIPTextEncode.encode(clip, negative_tags)
# Step 4: 根据 mode 分支准备 latent
if mode == "txt2img":
latent = EmptyLatentImage(w, h)
denoise = 1.0
elif mode == "img2img":
latent = vae.encode(resize(image, w, h))
denoise = plan["denoise"]
elif mode == "controlnet_pose":
if cn_type == "animalpose":
pose = AnimalPosePreprocessor(image)
elif cn_type == "openpose":
pose = DWOpenposePreprocessor(image)
else:
pose = DepthAnythingV2(image)
controlnet = load_controlnet(cn_model_path)
pos, neg = ControlNetApplyAdvanced(
pos, neg, controlnet, pose,
strength=plan["controlnet_strength"]
)
latent = EmptyLatentImage(w, h)
elif mode == "inpaint":
target = plan["inpaint_target"]
mask = AnimeSeg.segment(image, target)
mask = dilate(mask, 10px)
latent_samples = vae.encode(resize(image, w, h))
latent = {"samples": latent_samples, "noise_mask": mask}
denoise = plan["denoise"]
# Step 5: 采样(所有模式汇合)
samples = KSampler(
model=model, positive=pos, negative=neg,
latent_image=latent, denoise=denoise,
steps=steps, cfg=cfg, seed=seed
)
# Step 6: 解码
image = vae.decode(samples)
为什么不直接让 LLM 输出 ComfyUI workflow JSON?
考虑过让 LLM 直接生成完整的 ComfyUI API workflow JSON(节点 ID、连线关系等),但放弃了:
- Token 消耗巨大 — 一个完整 workflow JSON 动辄 200+ 行,7B 模型生成这么长的结构化输出容易出错
- 节点 API 不稳定 — ComfyUI 和自定义节点的 API 经常变化,LLM 的训练数据跟不上
- 调试困难 — LLM 生成的 JSON 如果有一个字段拼错,整个 workflow 就跑不了
- 不需要 — 实际上只有 5 种固定链路,LLM 只需要做”选择题”(选模式)和”填空题”(填参数),不需要”作文题”(写完整 workflow)
最终设计:LLM 做决策,代码做组装。LLM 输出一个轻量 JSON(~10 个字段),代码根据这些字段走预定义的分支逻辑,调用 ComfyUI 的 Python API。
Workflow JSON 导出(调试用):
虽然实际生成不走 ComfyUI workflow,但每次生成后会导出一份等价的 workflow JSON 到 workflows/debug/ 目录。这份 JSON 可以直接拖进 ComfyUI 界面运行,方便:
- 复现某次生成的结果
- 手动微调参数后重新生成
- 理解 LLM 做了什么决策
效果展示
txt2img — 纯文字生图
prompt: "一只白色猫娘坐在窗台上"
LLM 自动生成 tags: score_9, score_8_up, 1girl, cat_ears, white_hair, sitting, windowsill...
输出 1024×1024,约 10 秒(25 steps, euler_ancestral)。
controlnet_pose — 姿势保持
输入参考图 → prompt: "用这个姿势画一个穿铠甲的精灵"
LLM 判断: mode=controlnet_pose, controlnet_type=animalpose(检测到非人类二次元角色)
AnimalPose 提取骨骼关键点 → ControlNet 引导生成新角色,姿势一致但外观完全不同。
inpaint — AnimeSeg 局部重绘
输入参考图 → prompt: "给这个角色换一件红色连衣裙"
LLM 判断: mode=inpaint, inpaint_target=”clothing”
AnimeSeg DINOv2 分割出衣服区域(class 10, 覆盖率 ~10%)→ 膨胀 mask → 只重绘衣服部分,脸部和背景完全不变。
ratio 2:3 — 竖版构图
prompt: "一只蓝色的龙女穿着铠甲站在城堡前",ratio=2:3, size=1K
输出 680×1024 竖版构图。
踩坑记录
坑 1: ControlNet 模型格式不兼容
现象: ControlNetLoader 加载 .safetensors 报错 “Could not detect model type”
原因: SDXL ControlNet 有多种格式(full / LoRA / lite),ComfyUI 的 ControlNetLoader 对某些格式不兼容。
解决: 用 comfy.controlnet.load_controlnet() 直接加载,它内部会自动检测格式并转换。
坑 2: AnimalPose 预处理器找不到
现象: AnimalPosePreprocessor 节点不存在
原因: comfyui_controlnet_aux 默认不安装 AnimalPose 的依赖(MMPose/MMDet)。
解决:
pip install mmpose mmdet mmengine
# 模型会在首次使用时自动下载到 annotator 目录
坑 3: SAM3 模型无法加载
现象: ComfyUI 内置的 SAM3_Detect 节点需要 MODEL 类型输入,但 CheckpointLoaderSimple 和 UNETLoader 都无法加载 SAM2.1 权重。
原因: SAM3 是 ComfyUI 0.19+ 新增的内置节点,但模型加载器尚未完善,model_detection.py 无法识别 SAM2 的权重格式。
结论: 暂时放弃 SAM3,改用 AnimeSeg DINOv2 做二次元分割,效果更好。
坑 4: CLIPSeg 对二次元无效
现象: CLIPSegDetectorProvider 用 “clothing” 做 prompt,对二次元角色的衣服检测覆盖率 0%。
原因: CLIPSeg 基于 CLIP ViT-B/16,训练数据以真实照片为主,对动漫风格的语义理解很差。
解决: 换用 AnimeSeg DINOv2(专门在动漫数据集上训练),13 类分割准确率高。
坑 5: AnimeSeg Mask2Former 输出全黑
现象: AnimeSegPipeline.from_mask2former() 加载成功,但所有像素都预测为 class 0(background)。
原因: Mask2Former 变体的预训练权重可能有问题,或者推理 pipeline 的后处理逻辑不正确。
解决: 换用 from_dinoV2() 变体,316M 参数,分割效果正常。DINOv2 输出 [1, 13, 576, 576] 的 logits,直接 argmax 取类别。
坑 6: VAE Encode 返回值类型混淆
现象: vae.encode() 返回后传给 comfy.sample.prepare_noise() 报错 'dict' object has no attribute 'is_nested'
原因: ComfyUI 的 vae.encode() 直接返回 latent tensor,但 inpaint 路径需要把它包装成 {"samples": tensor, "noise_mask": mask} 格式。prepare_noise() 只接受 tensor,不接受 dict。
解决: inpaint 路径单独处理——从 dict 中取出 samples 传给 prepare_noise(),noise_mask 通过 comfy.sample.sample() 的 noise_mask 参数传入。
坑 7: 变量定义顺序导致 UnboundLocalError
现象: cannot access local variable 'plan' where it is not associated with a value
原因: ratio auto 逻辑里用了 plan.get("ratio"),但 plan = _plan_pipeline() 在后面才调用。
解决: 重新排列代码顺序——先调 LLM 获取 plan,再计算分辨率。
坑 8: 大分辨率导致画面撕裂
现象: 1536×2048 输出的图片出现画面重复、多个头/身体、构图混乱。
原因: SDXL/Pony 的原生训练分辨率约 1MP(1024×1024),1536×2048 = 3.1MP 超出 3 倍,模型在超出训练分辨率时会产生重复伪影。
解决: 生成阶段始终用 1K 分辨率,1.5K/2K 通过后处理 upscale 实现。
项目结构
spark-canvas/
├── custom_nodes/
│ └── spark-canvas/
│ └── __init__.py # 核心代码 (~1000 行)
├── docs/
│ ├── build-guide.md # 本文档
│ └── images/ # 效果展示图片
├── scripts/
│ └── reload_spark_canvas.sh # 热重载脚本
├── TODO.md
└── README.md
依赖的 ComfyUI 自定义节点:
| 节点包 | 用途 |
|---|---|
| comfyui_controlnet_aux | ControlNet 预处理器 (depth/openpose/animalpose) |
| ComfyUI_IPAdapter_plus | IP-Adapter 角色风格迁移 |
| ComfyUI-Impact-Pack | CLIPSeg、SAM 等辅助分割节点 |
Python 依赖:
anime-seg # AnimeSeg DINOv2 二次元分割
mmpose, mmdet # AnimalPose 预处理器依赖
vllm # LLM 推理服务
改进方向
短期可改进:
- Few-shot 示例 — 在 PLAN_PROMPT 里加入 3-5 个输入输出示例,让 LLM 更准确地模仿预期格式。当前是 zero-shot,全靠规则描述。
- 二次确认机制 — LLM 输出 plan 后,再调一次 LLM 做 self-check:”这个 plan 合理吗?用户说的是 X,你选了 Y 模式,确认?”。可以捕获明显的误判。
- 历史上下文 — 记住上一次生成的参数和结果,支持”再亮一点”、”denoise 调低一些”等迭代修改。当前每次生成都是独立的。
- 更细粒度的 inpaint_target — 当前 AnimeSeg 只有 13 类,无法区分”上衣”和”裤子”(都是 class 10)。可以结合 SAM(Segment Anything)做实例级分割。
中期架构演进: 5. 混合路线:预定义模式 + LLM 动态扩展 — 保留 5 种核心模式作为”快速通道”,同时允许 LLM 在遇到不匹配的需求时,回退到 Comfy-Cozy Agent 风格的动态 workflow 生成。 6. Reward Model / 用户反馈 — 收集用户对生成结果的满意度(点赞/点踩),用来微调 LLM 的 pipeline 选择策略。 7. 多步工作流 — 当前是单步生成。复杂需求(如”先换衣服,再换背景,最后加上光效”)需要拆解成多步 pipeline 串联执行。
长期目标: 8. 端到端训练 — 用 <用户 prompt, 参考图, 最佳 workflow 参数> 三元组数据,直接微调一个专门做 SD workflow 规划的小模型,替代通用 LLM + prompt engineering。 9. 自动化测试 — 建立 benchmark:100 个典型 prompt + 预期 mode/参数,自动评估 LLM 的规划准确率,每次改 prompt 后跑回归测试。
备注
搭建于 2026 年 4 月,基于 ComfyUI 0.19.3 + Pony Diffusion V6 XL + Qwen3.6-VL。
原文链接
原始笔记来源:收藏文章/从零搭建丐版 NanoBanana AI 绘图节点(一).md