Add Block Loop (Recursive) nodes

This commit is contained in:
t22m003
2025-12-06 22:46:32 +09:00
parent 4b7245f8a2
commit fedfd2a998
4 changed files with 843 additions and 0 deletions
+11
View File
@@ -9,6 +9,17 @@ This allows you to repeat execution steps by outputting a list of integers, whic
- **For Loop (Count)**: Simple loop that runs `N` times. outputs `0` to `N-1`.
- **For Loop (Range)**: Loop based on python `range(start, stop, step)`.
### Block Looping (Recursive)
- **Block Loop Start**: Marks the beginning of a loop.
- `first_value`: Value for the first iteration.
- `total_loops`: How many times to loop.
- **Block Loop End**: Marks the end of a loop.
- `flow_control`: Connect the last node of your loop block here.
- `next_value`: The value to pass to the next iteration (connects to `recycle_value` of Start internally).
- `loop_id`: Connect to the `loop_id` output of `Block Loop Start`.
- Note: Connect `total_loops` and `current_loop` from `Block Loop Start` to `Block Loop End`.
## Installation
Navigate to your `ComfyUI/custom_nodes/` directory and clone this repository:
+5
View File
@@ -1,12 +1,17 @@
from .loop_node import SimpleForLoop, SimpleForLoopRange
from .recursive_loop import BlockLoopStart, BlockLoopEnd
NODE_CLASS_MAPPINGS = {
"SimpleForLoop": SimpleForLoop,
"SimpleForLoopRange": SimpleForLoopRange,
"BlockLoopStart": BlockLoopStart,
"BlockLoopEnd": BlockLoopEnd,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SimpleForLoop": "For Loop (Count)",
"SimpleForLoopRange": "For Loop (Range)",
"BlockLoopStart": "Block Loop Start",
"BlockLoopEnd": "Block Loop End",
}
+713
View File
@@ -0,0 +1,713 @@
{
"id": "bcd98e17-ef91-4fef-88e3-4b80d71c77c8",
"revision": 0,
"last_node_id": 11,
"last_link_id": 15,
"nodes": [
{
"id": 9,
"type": "SaveImage",
"pos": [
1451,
189
],
"size": [
252.63381958007812,
58
],
"flags": {
"collapsed": true
},
"order": 7,
"mode": 0,
"inputs": [
{
"localized_name": "\u753b\u50cf",
"name": "images",
"type": "IMAGE",
"link": 9
},
{
"localized_name": "\u30d5\u30a1\u30a4\u30eb\u540d_\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9",
"name": "filename_prefix",
"type": "STRING",
"widget": {
"name": "filename_prefix"
},
"link": null
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68"
},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1209,
188
],
"size": [
210,
46
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"localized_name": "\u30b5\u30f3\u30d7\u30eb",
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"localized_name": "vae",
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"localized_name": "\u753b\u50cf",
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
9,
10
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68",
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
-120.81473541259766,
358.7232666015625
],
"size": [
315,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "ckpt\u540d",
"name": "ckpt_name",
"type": "COMBO",
"widget": {
"name": "ckpt_name"
},
"link": null
}
],
"outputs": [
{
"localized_name": "\u30e2\u30c7\u30eb",
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
11
]
},
{
"localized_name": "CLIP",
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": [
13
]
},
{
"localized_name": "VAE",
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": [
8
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68",
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"animagine-xl-4.0.safetensors"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
473,
609
],
"size": [
315,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "\u5e45",
"name": "width",
"type": "INT",
"widget": {
"name": "width"
},
"link": null
},
{
"localized_name": "\u9ad8\u3055",
"name": "height",
"type": "INT",
"widget": {
"name": "height"
},
"link": null
},
{
"localized_name": "\u30d0\u30c3\u30c1\u30b5\u30a4\u30ba",
"name": "batch_size",
"type": "INT",
"widget": {
"name": "batch_size"
},
"link": null
}
],
"outputs": [
{
"localized_name": "\u6f5c\u5728",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
2
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68",
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
896,
1152,
4
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
863,
186
],
"size": [
315,
262
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"localized_name": "\u30e2\u30c7\u30eb",
"name": "model",
"type": "MODEL",
"link": 12
},
{
"localized_name": "\u30dd\u30b8\u30c6\u30a3\u30d6",
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"localized_name": "\u30cd\u30ac\u30c6\u30a3\u30d6",
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"localized_name": "\u6f5c\u5728\u753b\u50cf",
"name": "latent_image",
"type": "LATENT",
"link": 2
},
{
"localized_name": "\u30b7\u30fc\u30c9",
"name": "seed",
"type": "INT",
"widget": {
"name": "seed"
},
"link": 16
},
{
"localized_name": "\u30b9\u30c6\u30c3\u30d7",
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": null
},
{
"localized_name": "cfg",
"name": "cfg",
"type": "FLOAT",
"widget": {
"name": "cfg"
},
"link": null
},
{
"localized_name": "\u30b5\u30f3\u30d7\u30e9\u30fc\u540d",
"name": "sampler_name",
"type": "COMBO",
"widget": {
"name": "sampler_name"
},
"link": null
},
{
"localized_name": "\u30b9\u30b1\u30b8\u30e5\u30fc\u30e9",
"name": "scheduler",
"type": "COMBO",
"widget": {
"name": "scheduler"
},
"link": null
},
{
"localized_name": "\u30ce\u30a4\u30ba\u9664\u53bb",
"name": "denoise",
"type": "FLOAT",
"widget": {
"name": "denoise"
},
"link": null
}
],
"outputs": [
{
"localized_name": "\u6f5c\u5728",
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
7
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68",
"Node name for S&R": "KSampler"
},
"widgets_values": [
835478302784344,
"randomize",
28,
15,
"euler",
"normal",
1
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
413,
389
],
"size": [
425.27801513671875,
180.6060791015625
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"localized_name": "\u30af\u30ea\u30c3\u30d7",
"name": "clip",
"type": "CLIP",
"link": 15
},
{
"localized_name": "\u30c6\u30ad\u30b9\u30c8",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "\u6761\u4ef6\u4ed8\u3051",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
6
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68",
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"low quality, bad anatomy, wrong proportions, blurry, missing fingers, extra fingers, extra limbs, deformed hands, mutated hands, cropped body, missing shoes, barefoot, distorted perspective, floating body parts, poor lighting, messy background, oversaturated, underexposed, cartoonish, animal ears, wolf ears, cat ears, fox ears, anthropomorphic traits, feminine traits, cleavage, large chest, golden color, golden hair, golden accessories, golden elements\n"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
415,
186
],
"size": [
422.84503173828125,
164.31304931640625
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"localized_name": "\u30af\u30ea\u30c3\u30d7",
"name": "clip",
"type": "CLIP",
"link": 14
},
{
"localized_name": "\u30c6\u30ad\u30b9\u30c8",
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": null
}
],
"outputs": [
{
"localized_name": "\u6761\u4ef6\u4ed8\u3051",
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
4
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68",
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"1 man, black wolf hair with red tips, red eyes, scowling with a scornful expression, slim build, seated on an ornate throne, elbow resting on the armrest, one leg crossed over the other, both shoes clearly visible and worn, black dress shoes, viewed from a slightly elevated frontal angle (moderately looking down at him), natural perspective, cinematic lighting from a large glass window behind, night view outside, perfect anatomy, realistic hand structure, detailed clothing folds, elegant but intimidating aura, masterpiece, high detail, mystyle1004, absurdres, anime style"
]
},
{
"id": 10,
"type": "PreviewImage",
"pos": [
878.84619140625,
499.4828369226669
],
"size": [
569.770751953125,
532.2428588867188
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"localized_name": "\u753b\u50cf",
"name": "images",
"type": "IMAGE",
"link": 10
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 11,
"type": "LoraLoader",
"pos": [
-9.72401907897563,
549.675537109375
],
"size": [
432.13029959655375,
126
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "\u30e2\u30c7\u30eb",
"name": "model",
"type": "MODEL",
"link": 11
},
{
"localized_name": "\u30af\u30ea\u30c3\u30d7",
"name": "clip",
"type": "CLIP",
"link": 13
},
{
"localized_name": "lora_name",
"name": "lora_name",
"type": "COMBO",
"widget": {
"name": "lora_name"
},
"link": null
},
{
"localized_name": "\u30e2\u30c7\u30eb\u306e\u5f37\u5ea6",
"name": "strength_model",
"type": "FLOAT",
"widget": {
"name": "strength_model"
},
"link": null
},
{
"localized_name": "\u30af\u30ea\u30c3\u30d7\u306e\u5f37\u5ea6",
"name": "strength_clip",
"type": "FLOAT",
"widget": {
"name": "strength_clip"
},
"link": null
}
],
"outputs": [
{
"localized_name": "\u30e2\u30c7\u30eb",
"name": "MODEL",
"type": "MODEL",
"links": [
12
]
},
{
"localized_name": "CLIP",
"name": "CLIP",
"type": "CLIP",
"links": [
14,
15
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.68",
"Node name for S&R": "LoraLoader"
},
"widgets_values": [
"ComfyUI_trained_lora_00003_.safetensors",
1,
1
]
},
{
"id": 12,
"type": "SimpleForLoop",
"pos": [
400,
100
],
"size": {
"0": 210,
"1": 58
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "index",
"type": "INT",
"links": [
16
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "SimpleForLoop"
},
"widgets_values": [
4
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
10,
8,
0,
10,
0,
"IMAGE"
],
[
11,
4,
0,
11,
0,
"MODEL"
],
[
12,
11,
0,
3,
0,
"MODEL"
],
[
13,
4,
1,
11,
1,
"CLIP"
],
[
14,
11,
1,
6,
0,
"CLIP"
],
[
15,
11,
1,
7,
0,
"CLIP"
],
[
16,
12,
0,
3,
4,
"INT"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.5982842210331787,
"offset": [
805.7871857209614,
-76.79577922697746
]
}
},
"version": 0.4
}
+114
View File
@@ -0,0 +1,114 @@
import json
import uuid
from server import PromptServer
from aiohttp import web
class BlockLoopStart:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"first_value": ("INT", {"default": 1}),
"total_loops": ("INT", {"default": 3, "min": 1, "max": 100}),
},
"optional": {
"current_loop": ("INT", {"default": 0, "hidden": True}), # Injectable
"recycle_value": ("INT", {"default": 0, "hidden": True}), # Injectable from previous loop
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("INT", "INT", "INT", "STRING")
RETURN_NAMES = ("current_value", "index", "total", "loop_id")
FUNCTION = "execute"
CATEGORY = "Loop"
def execute(self, first_value, total_loops, current_loop=0, recycle_value=0, unique_id=None):
# On first run (current_loop=0), use first_value.
# On subsequent runs, use recycle_value.
if current_loop == 0:
val = first_value
else:
val = recycle_value
return (val, current_loop, total_loops, unique_id)
class BlockLoopEnd:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"flow_control": ("*",), # Anything to enforce order
"next_value": ("INT",), # Value to pass to next iteration
"total_loops": ("INT", {"forceInput": True}),
"current_loop": ("INT", {"forceInput": True}),
"loop_id": ("STRING", {"forceInput": True}), # Link to Start Node
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("flow_output",)
FUNCTION = "execute"
CATEGORY = "Loop"
OUTPUT_NODE = True
def execute(self, flow_control, next_value, total_loops, current_loop, loop_id, prompt=None, extra_pnginfo=None):
if current_loop + 1 < total_loops:
# Prepare next iteration
new_loop_index = current_loop + 1
print(f"Looping: {current_loop} -> {new_loop_index} / {total_loops}")
# We need to find the specific BlockLoopStart node in the prompt and update its inputs.
# loop_id is the UNIQUE_ID of the BlockLoopStart node.
new_prompt = prompt.copy()
target_node = new_prompt.get(str(loop_id))
if target_node:
# Update inputs for the next run
target_node['inputs']['current_loop'] = new_loop_index
target_node['inputs']['recycle_value'] = next_value
# Re-queue
new_prompt_id = str(uuid.uuid4())
async def queue_new_prompt():
import execution
server = PromptServer.instance
# Validate
valid = await execution.validate_prompt(new_prompt_id, new_prompt)
if valid[0]:
outputs_to_execute = valid[2]
# We need to get the next execution number
server.number += 1
number = server.number
# Extra data
new_extra_data = extra_pnginfo.copy() if extra_pnginfo else {}
# Put in queue
server.prompt_queue.put((number, new_prompt_id, new_prompt, new_extra_data, outputs_to_execute, {}))
print(f"Loop queued: {new_prompt_id}")
else:
print(f"Loop validation failed: {valid[1]}")
# Schedule the task
PromptServer.instance.loop.create_task(queue_new_prompt())
return (flow_control,)