概述

一个 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 JSON
  • SparkCanvas.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 工程的关键设计:

  1. 每种 mode 的触发条件用中英文双语列出,避免 LLM 对中文指令理解偏差:
  • txt2img: 用户想从零生成 / “画一个” / “生成” / no reference needed
  • inpaint: 用户想修改图片的特定部分 / “换衣服” / “改头发” / “change clothing”
  1. ratio 规则需要明确”什么时候不跟原图”:
  • 用户说”调整比例/换画幅/自由发挥” → LLM 自己选比例
  • 用户说”竖图/手机壁纸” → “2:3” 或 “9:16”
  • 没提到比例 → “auto”(系统层面跟随原图)
  1. 多模态视觉理解的实际作用——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、连线关系等),但放弃了:

  1. Token 消耗巨大 — 一个完整 workflow JSON 动辄 200+ 行,7B 模型生成这么长的结构化输出容易出错
  2. 节点 API 不稳定 — ComfyUI 和自定义节点的 API 经常变化,LLM 的训练数据跟不上
  3. 调试困难 — LLM 生成的 JSON 如果有一个字段拼错,整个 workflow 就跑不了
  4. 不需要 — 实际上只有 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 类型输入,但 CheckpointLoaderSimpleUNETLoader 都无法加载 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 推理服务

改进方向

短期可改进:

  1. Few-shot 示例 — 在 PLAN_PROMPT 里加入 3-5 个输入输出示例,让 LLM 更准确地模仿预期格式。当前是 zero-shot,全靠规则描述。
  2. 二次确认机制 — LLM 输出 plan 后,再调一次 LLM 做 self-check:”这个 plan 合理吗?用户说的是 X,你选了 Y 模式,确认?”。可以捕获明显的误判。
  3. 历史上下文 — 记住上一次生成的参数和结果,支持”再亮一点”、”denoise 调低一些”等迭代修改。当前每次生成都是独立的。
  4. 更细粒度的 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