api/reference_blocks.py holds groups distilled from workflows that do one job well: a repaint that crops around the mark, repaints at 1024 and stitches back (ComfyUI-Inpaint-CropAndStitch), the same with built-in nodes only, and face and hand detailing (Impact Pack, the detector models this ComfyUI has). begin_task hands the AI their directory; add_nodes(block=...) builds one in one call. Its links name roles web/modules/canvas_roles.js finds on the canvas by their sockets (the sampler, its decode and save, what feeds an input upstream), so canvas_wiring splices it into the workflow there; several samplers need near_node_id, a missing pack names the pack and the stand-in. The repaint note now points to the block. Checked on the test ComfyUI: the repaint changed the marked area only, the detailer the face area. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
7.7 KiB
Python
122 lines
7.7 KiB
Python
"""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. 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.
|
||
"""
|
||
|
||
BLOCKS = {
|
||
"inpaint_crop_stitch": {
|
||
"for": "Repaint a marked area (inpaint_image) of a picture: crops around it, repaints it at 1024 and stitches "
|
||
"it back, the rest untouched. Best for faces, hands, small fixes.",
|
||
"packs": {"InpaintCropImproved": "ComfyUI-Inpaint-CropAndStitch", "InpaintStitchImproved": "ComfyUI-Inpaint-CropAndStitch"},
|
||
"instead": "inpaint_basic",
|
||
"takes": "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": 1.2,
|
||
"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"},
|
||
},
|
||
"after": "The sampler now repaints the cropped area (its steps, cfg and sampler stay; denoise 1 repaints it "
|
||
"fully, 0.6–0.8 keeps more of the old). run_workflow; the saved picture is the whole one, stitched.",
|
||
},
|
||
"inpaint_basic": {
|
||
"for": "Repaint a marked area (inpaint_image) with ComfyUI's own nodes only: the whole picture is sampled, "
|
||
"only the area changes.",
|
||
"packs": {},
|
||
"takes": "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"},
|
||
},
|
||
"after": "Set the sampler's denoise to 0.6–0.8 (set_values) so the area keeps the picture's look, then run_workflow.",
|
||
},
|
||
"detail_face_hands": {
|
||
"for": "Fix faces and hands after a picture is made: finds each one, repaints it larger and puts it back "
|
||
"(FaceDetailer). For a picture made on this canvas.",
|
||
"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):
|
||
"""{block: what it is for} for the AI; with ComfyUI's node list `info`, a block whose nodes are missing says what to install."""
|
||
out = {}
|
||
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})
|
||
out[name] = block["for"] + (f" Needs {', '.join(missing)} (not installed: use {block['instead']})."
|
||
if missing and block.get("instead") else f" Needs {', '.join(missing)} (not installed)." if missing else "")
|
||
return out
|
||
|
||
|
||
def _detector(info, prefix):
|
||
"""The detector model file for `prefix` this ComfyUI has, preferring the larger kinds (m over s over n)."""
|
||
try:
|
||
files = info["UltralyticsDetectorProvider"]["input"]["required"]["model_name"][0]
|
||
except (KeyError, IndexError, TypeError):
|
||
files = []
|
||
found = sorted((name for name in files if isinstance(name, str) and 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 nodes_of(name, info, image=None):
|
||
"""Block `name` as add_nodes' group, its values filled in, or BlockError saying why not."""
|
||
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") == "image" and not (isinstance(image, str) and image.strip()):
|
||
raise BlockError(f"Block {name} needs image: the picture with the area as its alpha (inpaint_image from "
|
||
"propose_repaint_area or ask_user's answer).")
|
||
fills = {"$image": (image or "").strip()}
|
||
for role, prefix in (block.get("detectors") or {}).items():
|
||
fills[f"${role}"] = _detector(info, prefix)
|
||
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 after(name):
|
||
return BLOCKS[name]["after"]
|