Files
DemonGatanjieu-Anomalous_Mo…/api/reference_blocks.py
T
DemonGatanjieuandClaude Opus 5.5 5d023f3a7c Blocks: LanPaint repaint, upscale, tiled upscale, pose and depth ControlNets
A block node can be "like" a role (takes its settings) or "replace" it (its
links, place and settings too; LanPaint's sampler for the KSampler). $save is
the output node the picture reaches, past a stitch. Upscale blocks take an
upscale model this ComfyUI has; control blocks a ControlNet whose base fits the
canvas's model, and the picture to follow copied into the input folder. The
directory is grouped by job, a few words a block, to keep begin_task short.
Drop-down choices are read in both forms of ComfyUI's node list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 02:01:09 +08:00

275 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Ready-made blocks of nodes, distilled from workflows that do one thing well (a repaint that
crops around the marked area and stitches it back, face and hand detailing): a light model
names one (add_nodes block=…) and Anomalous builds it on the canvas, spliced into the
workflow there. A block is a group in ComfyUI's API form (mcp_canvas_edit.add_nodes) whose
links may name roles the page finds on the canvas by their sockets (web/modules/canvas_roles.js):
"$sampler" (the KSampler), "$decode" (the node decoding its latent), "$save" (the node taking
that picture); ["$decode", 0] is that node's output, ["$sampler.positive", "*"] what feeds that
input, a path ("$sampler.positive.clip") followed upstream. A node "like" a role takes its
settings of the same name; one that "replaces" a role takes its place, links and settings. The
directory (directory()) is the "reference" the AI is handed when it begins a task: one line a block,
the rest comes with the block.
"""
import re
# A redraw after enlarging: low enough to keep the picture, high enough for real detail (the source used 0.32),
# at the size the source used (its author: more than 1.5× goes wrong; 1.5× of 832×1216 is too much for a 6 GB card).
UPSCALE_DENOISE = 0.35
UPSCALE_BY = 1.25
INPAINT_IMAGE = "the picture with the area as its alpha (inpaint_image from propose_repaint_area or ask_user's answer)"
REFERENCE_IMAGE = "the picture to follow: one the user handed you (its path, as their message gives it) or one in the input folder"
# The words a ControlNet file's name has for what it follows; a union model follows all of them.
CONTROLS = {"pose": ("openpose", "pose"), "depth": ("depth",)}
ANY_CONTROL = ("union", "promax")
# The directory's headings, what an AI wants done; under each, its blocks with a few words apiece.
JOBS = {"repaint": "Repaint a marked area", "upscale": "Enlarge a picture", "follow": "Follow a picture the user gave",
"detail": "Fix faces and hands"}
BLOCKS = {
"inpaint_crop_stitch": {
"job": "repaint", "short": "small fixes, the rest untouched",
"packs": {"InpaintCropImproved": "ComfyUI-Inpaint-CropAndStitch", "InpaintStitchImproved": "ComfyUI-Inpaint-CropAndStitch"},
"instead": "inpaint_basic",
"takes": INPAINT_IMAGE,
"nodes": {
"picture": {"class_type": "LoadImage", "inputs": {"image": "$image"}},
"soft": {"class_type": "DifferentialDiffusion", "inputs": {"model": ["$sampler.model", "*"]}, "wire_to": "$sampler"},
"crop": {"class_type": "InpaintCropImproved", "inputs": {
"image": ["picture", 0], "mask": ["picture", 1], "mask_blend_pixels": 32, "context_from_mask_extend_factor": 2.0,
"output_resize_to_target_size": True, "output_target_width": 1024, "output_target_height": 1024,
"downscale_algorithm": "lanczos", "upscale_algorithm": "lanczos"}},
"condition": {"class_type": "InpaintModelConditioning", "inputs": {
"vae": ["$decode.vae", "*"], "pixels": ["crop", "cropped_image"], "mask": ["crop", "cropped_mask"]},
"wire_to": "$sampler"},
"stitch": {"class_type": "InpaintStitchImproved", "inputs": {"stitcher": ["crop", "stitcher"], "inpainted_image": ["$decode", 0]},
"wire_to": "$save"},
},
"roles": {"sampler": {"denoise": 0.75}}, # at 1 a model not made for inpainting paints a whole new subject into the crop
"after": "The sampler now repaints only the cropped area, at denoise 0.75 (lower keeps more of the old). Its "
"prompt is still the whole picture's: set_prompt it to what belongs in the area (\"a hand, five "
"fingers\"), or it paints the whole subject there. Then run_workflow; the saved picture is the whole "
"one, stitched.",
},
"inpaint_basic": {
"job": "repaint", "short": "built-in nodes only",
"packs": {},
"takes": INPAINT_IMAGE,
"nodes": {
"picture": {"class_type": "LoadImage", "inputs": {"image": "$image"}},
"soft": {"class_type": "DifferentialDiffusion", "inputs": {"model": ["$sampler.model", "*"]}, "wire_to": "$sampler"},
"encode": {"class_type": "VAEEncode", "inputs": {"pixels": ["picture", 0], "vae": ["$decode.vae", "*"]}},
"area": {"class_type": "SetLatentNoiseMask", "inputs": {"samples": ["encode", 0], "mask": ["picture", 1]},
"wire_to": "$sampler"},
},
"roles": {"sampler": {"denoise": 0.7}},
"after": "The sampler repaints only the area, at denoise 0.7 so it keeps the picture's look (higher changes more). "
"Then run_workflow.",
},
"inpaint_lanpaint": {
"job": "repaint", "short": "best blend, slow",
"packs": {"LanPaint_KSampler": "LanPaint", "LanPaint_ImageEncode": "LanPaint", "LanPaint_ImageDecode": "LanPaint"},
"instead": "inpaint_crop_stitch",
"takes": INPAINT_IMAGE,
"nodes": {
"picture": {"class_type": "LoadImage", "inputs": {"image": "$image"}},
"encode": {"class_type": "LanPaint_ImageEncode", "inputs": {"image": ["picture", 0], "vae": ["$decode.vae", "*"], "mask": ["picture", 1]}},
"sampler": {"class_type": "LanPaint_KSampler", "replaces": "$sampler", "inputs": {"latent_image": ["encode", 0], "denoise": 1.0}},
"decode": {"class_type": "LanPaint_ImageDecode", "replaces": "$decode", "inputs": {
"samples": ["sampler", 0], "image": ["picture", 0], "mask": ["picture", 1]}},
},
"after": "The KSampler is now LanPaint's (same steps, cfg and seed) and repaints only the area, blending it "
"into the rest. Its prompt is still the whole picture's: set_prompt it to what belongs in the area "
"(\"a hand, five fingers\"). Then run_workflow; it takes several times longer than before.",
},
"upscale": {
"job": "upscale", "short": "1.25×",
"packs": {},
"upscaler": True,
"nodes": {
"upscaler": {"class_type": "UpscaleModelLoader", "inputs": {"model_name": "$upscaler"}}, # enlarges, then fit brings it to UPSCALE_BY
"enlarge": {"class_type": "ImageUpscaleWithModel", "inputs": {"upscale_model": ["upscaler", 0], "image": ["$save.images", "*"]}},
"fit": {"class_type": "ImageScaleBy", "inputs": {"image": ["enlarge", 0], "upscale_method": "lanczos", "scale_by": "$fit"}},
"encode": {"class_type": "VAEEncode", "inputs": {"pixels": ["fit", 0], "vae": ["$decode.vae", "*"]}},
"redraw": {"class_type": "KSampler", "like": "$sampler", "inputs": {
"model": ["$sampler.model", "*"], "positive": ["$sampler.positive", "*"], "negative": ["$sampler.negative", "*"],
"latent_image": ["encode", 0], "denoise": UPSCALE_DENOISE}},
"decode": {"class_type": "VAEDecode", "inputs": {"samples": ["redraw", 0], "vae": ["$decode.vae", "*"]}},
"save": {"class_type": "SaveImage", "inputs": {"images": ["decode", 0], "filename_prefix": "upscaled"}},
},
"after": f"run_workflow saves the picture as before and the larger one beside it (prefix upscaled). The redraw "
f"sampler has the first one's steps and cfg at denoise {UPSCALE_DENOISE}: lower keeps more of the "
"picture, higher adds more (and above 0.5 may change it). For 2× or more use upscale_tiled.",
},
"upscale_tiled": {
"job": "upscale", "short": "2×, in tiles",
"packs": {"UltimateSDUpscale": "ComfyUI_UltimateSDUpscale"},
"instead": "upscale",
"upscaler": True,
"nodes": {
"upscaler": {"class_type": "UpscaleModelLoader", "inputs": {"model_name": "$upscaler"}},
"tiles": {"class_type": "UltimateSDUpscale", "like": "$sampler", "inputs": {
"image": ["$save.images", "*"], "model": ["$sampler.model", "*"], "positive": ["$sampler.positive", "*"],
"negative": ["$sampler.negative", "*"], "vae": ["$decode.vae", "*"], "upscale_model": ["upscaler", 0],
"upscale_by": 2.0, "denoise": 0.3, "tile_width": 1024, "tile_height": 1024}},
"save": {"class_type": "SaveImage", "inputs": {"images": ["tiles", 0], "filename_prefix": "upscaled"}},
},
"after": "run_workflow saves the picture as before and the 2× one beside it (prefix upscaled); each tile is "
"redrawn with the sampler's steps and cfg at denoise 0.3 (higher adds more detail, and may add seams).",
},
"control_pose": {
"job": "follow", "short": "its pose",
"packs": {"OpenposePreprocessor": "comfyui_controlnet_aux"},
"takes": REFERENCE_IMAGE,
"control": "pose",
"nodes": {
"reference": {"class_type": "LoadImage", "inputs": {"image": "$image"}},
"pose": {"class_type": "OpenposePreprocessor", "inputs": {
"image": ["reference", 0], "detect_hand": "enable", "detect_body": "enable", "detect_face": "enable", "resolution": 1024}},
"controlnet": {"class_type": "ControlNetLoader", "inputs": {"control_net_name": "$controlnet"}},
"steer": {"class_type": "ControlNetApplyAdvanced", "inputs": {
"positive": ["$sampler.positive", "*"], "negative": ["$sampler.negative", "*"], "control_net": ["controlnet", 0],
"image": ["pose", 0], "vae": ["$decode.vae", "*"], "strength": 0.8, "start_percent": 0.0, "end_percent": 0.8},
"wire_to": "$sampler"},
},
"after": "run_workflow: the picture now takes that pose (strength 0.8, for the first 80% of the steps; lower "
"either for a looser pose). The prompt still says who and what: change it if it fights the pose.",
},
"control_depth": {
"job": "follow", "short": "its layout and shapes",
"packs": {"DepthAnythingV2Preprocessor": "comfyui_controlnet_aux"},
"takes": REFERENCE_IMAGE,
"control": "depth",
"nodes": {
"reference": {"class_type": "LoadImage", "inputs": {"image": "$image"}},
"depth": {"class_type": "DepthAnythingV2Preprocessor", "inputs": {
"image": ["reference", 0], "ckpt_name": "depth_anything_v2_vitl.pth", "resolution": 1024}},
"controlnet": {"class_type": "ControlNetLoader", "inputs": {"control_net_name": "$controlnet"}},
"steer": {"class_type": "ControlNetApplyAdvanced", "inputs": {
"positive": ["$sampler.positive", "*"], "negative": ["$sampler.negative", "*"], "control_net": ["controlnet", 0],
"image": ["depth", 0], "vae": ["$decode.vae", "*"], "strength": 0.7, "start_percent": 0.0, "end_percent": 0.7},
"wire_to": "$sampler"},
},
"after": "run_workflow: the picture now keeps that layout (strength 0.7, for the first 70% of the steps; lower "
"either to let the model change more). The prompt still says what is drawn.",
},
"detail_face_hands": {
"job": "detail", "short": "after a picture is made",
"packs": {"FaceDetailer": "ComfyUI-Impact-Pack", "UltralyticsDetectorProvider": "ComfyUI-Impact-Subpack"},
"detectors": {"face": "face_yolov8", "hand": "hand_yolov8"},
"nodes": {
"face_finder": {"class_type": "UltralyticsDetectorProvider", "inputs": {"model_name": "$face"}},
"hand_finder": {"class_type": "UltralyticsDetectorProvider", "inputs": {"model_name": "$hand"}},
"face": {"class_type": "FaceDetailer", "inputs": {
"image": ["$decode", 0], "model": ["$sampler.model", "*"], "clip": ["$sampler.positive.clip", "*"],
"vae": ["$decode.vae", "*"], "positive": ["$sampler.positive", "*"], "negative": ["$sampler.negative", "*"],
"bbox_detector": ["face_finder", 0], "denoise": 0.4, "guide_size": 512}},
"hands": {"class_type": "FaceDetailer", "inputs": {
"image": ["face", "image"], "model": ["$sampler.model", "*"], "clip": ["$sampler.positive.clip", "*"],
"vae": ["$decode.vae", "*"], "positive": ["$sampler.positive", "*"], "negative": ["$sampler.negative", "*"],
"bbox_detector": ["hand_finder", 0], "denoise": 0.45, "guide_size": 512}},
"save": {"class_type": "SaveImage", "inputs": {"images": ["hands", "image"], "filename_prefix": "detailed"}},
},
"after": "run_workflow saves the picture as before and the detailed one beside it (prefix detailed).",
},
}
class BlockError(ValueError):
pass
def directory(info=None):
"""{job: "block (a few words), …"} for the AI, short enough to stay in the chat; with ComfyUI's node
list `info`, a block whose nodes are missing says what to install and what to use instead."""
out = {heading: [] for heading in JOBS.values()}
for name, block in BLOCKS.items():
missing = sorted({pack for kind, pack in block["packs"].items() if info is not None and kind not in info})
needs = f"; needs {', '.join(missing)}" + (f", use {block['instead']}" if block.get("instead") else "") if missing else ""
out[JOBS[block["job"]]].append(f"{name} ({block['short']}{needs})")
return {heading: ", ".join(names) for heading, names in out.items()}
def _choices(info, kind, name):
"""The files node `kind`'s drop-down `name` offers, in either form ComfyUI's node list gives it
([[choices], {...}] or ["COMBO", {"options": [choices]}])."""
try:
spec = info[kind]["input"]["required"][name]
found = spec[0] if isinstance(spec[0], list) else spec[1].get("options", []) if spec[0] == "COMBO" else []
except (KeyError, IndexError, TypeError, AttributeError):
found = []
return [item for item in found if isinstance(item, str)]
def _detector(info, prefix):
"""The detector model file for `prefix` this ComfyUI has, preferring the larger kinds (m over s over n)."""
found = sorted((name for name in _choices(info, "UltralyticsDetectorProvider", "model_name") if prefix in name.lower()),
key=lambda name: ("m." not in name, "s." not in name, name))
if not found:
raise BlockError(f"No {prefix.split('_')[0]} detector model in models/ultralytics/bbox ({prefix}*.pt): "
"ask the user to get one (ComfyUI-Manager's model list has them).")
return found[0]
def _upscaler(info):
"""The upscale model file this ComfyUI has, preferring a 4× one, and how many times it enlarges."""
files = _choices(info, "UpscaleModelLoader", "model_name")
if not files:
raise BlockError("No upscale model in models/upscale_models: ask the user to get one (4x-UltraSharp, "
"ComfyUI-Manager's model list has it).")
times = {name: int(found.group(1) or found.group(2)) for name in files
if (found := re.search(r"(?<![0-9])([1-8])x|x([1-8])(?![0-9])", name.lower()))}
name = sorted(files, key=lambda item: (times.get(item) != 4, item.lower()))[0]
return name, times.get(name, 4)
def _controlnet(info, kind, main, base_of):
"""The ControlNet file for `kind` (CONTROLS) whose base model (`base_of(name)`) is of the family of
`main`, the canvas's model's: one for that kind before a union one, one of that very base first.
LLLite files need their own loader."""
from .model_advice import family
wanted = family(main)
files = [name for name in _choices(info, "ControlNetLoader", "control_net_name") if "lllite" not in name.lower()]
own = [name for name in files if any(word in name.lower() for word in CONTROLS[kind])]
union = [name for name in files if any(word in name.lower() for word in ANY_CONTROL) and name not in own]
fitting = [name for name in own + union if not wanted or family(base_of(name)) == wanted]
if not fitting:
raise BlockError(f"No {kind} ControlNet{f' for {wanted} models' if wanted else ''} in models/controlnet: ask the "
"user to get one (Anomalous can download one they pick on Civitai).")
return min(fitting, key=lambda name: (name in union, base_of(name).lower() != str(main).lower()))
def nodes_of(name, info, image=None, main_base=lambda: "", base_of=lambda name: ""):
"""Block `name` as add_nodes' group, its values filled in, or BlockError saying why not. A control
block asks `main_base()` for the canvas's model's base model and `base_of` for each ControlNet's."""
block = BLOCKS.get(name)
if not block:
raise BlockError(f"No block {name!r}; there are: {', '.join(BLOCKS)}.")
missing = sorted({pack for kind, pack in block["packs"].items() if kind not in info})
if missing:
instead = f" Use block {block['instead']} instead, or ask the user to install it with ComfyUI-Manager." if block.get("instead") else \
" Ask the user to install it with ComfyUI-Manager."
raise BlockError(f"Block {name} needs {', '.join(missing)}, not installed here.{instead}")
if block.get("takes") and not (isinstance(image, str) and image.strip()):
raise BlockError(f"Block {name} needs image: {block['takes']}.")
fills = {"$image": (image or "").strip()}
for role, prefix in (block.get("detectors") or {}).items():
fills[f"${role}"] = _detector(info, prefix)
if block.get("upscaler"):
fills["$upscaler"], times = _upscaler(info)
fills["$fit"] = round(UPSCALE_BY / times, 4) # the model's 4× brought back to UPSCALE_BY
if block.get("control"):
fills["$controlnet"] = _controlnet(info, block["control"], main_base(), base_of)
return {key: {**spec, "inputs": {input: fills.get(value, value) if isinstance(value, str) else value
for input, value in spec["inputs"].items()}} for key, spec in block["nodes"].items()}
def roles_of(name):
"""The settings block `name` changes on canvas nodes it finds ({role: {setting: value}}), as the page applies them."""
return BLOCKS[name].get("roles") or {}
def after(name):
return BLOCKS[name]["after"]