Files
BizaNator 9f005e8b0e feat: Add seed management with INT output for KSampler connection (v2.1.0)
- Added INT seed output to PromptIteratorDynamic and PromptIteratorAdvanced
- Implemented 4 seed modes: fixed, increment_batch, increment_prompt, random
- Fixed mode: Same seed for all prompts
- Increment batch: Increments seed when restarting prompt list
- Increment prompt: Increments seed on each prompt
- Random: Random seed for each generation
- Added generation_seed input parameter (-1 for auto)
- Seed persistence across workflow executions
- Updated README with seed management documentation
- Bumped version to 2.1.0
2025-09-24 11:46:50 -05:00

651 lines
24 KiB
Python

"""
Prompt Iterator Nodes for ComfyUI
Provides nodes for iterating through multiple prompts with automatic filename generation
Author: BiloxiStudios Inc - BizaNator
Version: 2.1.0
"""
import json
import random
from typing import Dict, List, Tuple, Any, Optional
# Global state management for tracking iteration position
ITERATOR_STATE: Dict[str, Dict[str, Any]] = {}
class PromptIteratorDynamic:
"""
Dynamic prompt iterator node that accepts multiple string inputs
and cycles through them with automatic filename generation
"""
@classmethod
def INPUT_TYPES(cls):
inputs = {
"required": {
"mode": (["sequential", "manual", "random", "single"], {
"default": "sequential"
}),
"filename_mode": (["auto_index", "suffix_list", "template"], {
"default": "auto_index"
}),
"base_filename": ("STRING", {
"default": "output",
"multiline": False
}),
},
"optional": {
"prompt_1": ("STRING", {
"multiline": True,
"default": "",
"forceInput": True,
"dynamicPrompts": False
}),
"prompt_2": ("STRING", {
"multiline": True,
"default": "",
"forceInput": True,
"dynamicPrompts": False
}),
"prompt_3": ("STRING", {
"multiline": True,
"default": "",
"forceInput": True,
"dynamicPrompts": False
}),
"prompt_4": ("STRING", {
"multiline": True,
"default": "",
"forceInput": True,
"dynamicPrompts": False
}),
"prompt_5": ("STRING", {
"multiline": True,
"default": "",
"forceInput": True,
"dynamicPrompts": False
}),
"prompt_6": ("STRING", {
"multiline": True,
"default": "",
"forceInput": True,
"dynamicPrompts": False
}),
"prompt_7": ("STRING", {
"multiline": True,
"default": "",
"forceInput": True,
"dynamicPrompts": False
}),
"prompt_8": ("STRING", {
"multiline": True,
"default": "",
"forceInput": True,
"dynamicPrompts": False
}),
"suffixes": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": False,
"placeholder": "One suffix per line (for suffix_list mode)"
}),
"filename_template": ("STRING", {
"default": "{base}_{index:03d}",
"multiline": False,
"placeholder": "{base}, {index}, {suffix}"
}),
"manual_index": ("INT", {
"default": 0,
"min": 0,
"max": 9999,
"step": 1
}),
"reset": ("BOOLEAN", {
"default": False,
"label_on": "Reset",
"label_off": "Continue"
}),
"generation_seed": ("INT", {
"default": -1,
"min": -1,
"max": 2147483647,
"step": 1,
"display": "number"
}),
"seed_mode": (["fixed", "increment_batch", "increment_prompt", "random"], {
"default": "increment_batch"
}),
"workflow_id": ("STRING", {
"default": "default",
"multiline": False
}),
}
}
# Add more prompt inputs dynamically (up to 20)
for i in range(9, 21):
inputs["optional"][f"prompt_{i}"] = ("STRING", {
"multiline": True,
"default": "",
"forceInput": True,
"dynamicPrompts": False
})
return inputs
RETURN_TYPES = ("STRING", "STRING", "INT", "INT", "STRING", "INT")
RETURN_NAMES = ("prompt", "filename", "current_index", "total_count", "status", "seed")
FUNCTION = "iterate_prompts"
CATEGORY = "utils/prompt"
OUTPUT_NODE = False
@classmethod
def IS_CHANGED(cls, **kwargs):
"""Force re-execution when inputs change"""
mode = kwargs.get("mode", "sequential")
if mode in ["sequential", "random"]:
return float("NaN")
return False
def iterate_prompts(self, mode: str, filename_mode: str, base_filename: str,
suffixes: str = "", filename_template: str = "",
manual_index: int = 0, reset: bool = False,
generation_seed: int = -1, seed_mode: str = "increment_batch",
workflow_id: str = "default", **kwargs) -> Tuple:
"""
Main execution function for dynamic prompt iteration
"""
global ITERATOR_STATE
# Collect all prompt inputs dynamically
prompt_list = []
for i in range(1, 21): # Check up to 20 prompt inputs
prompt_key = f"prompt_{i}"
if prompt_key in kwargs and kwargs[prompt_key]:
prompt_value = kwargs[prompt_key]
if isinstance(prompt_value, str) and prompt_value.strip():
prompt_list.append(prompt_value.strip())
if not prompt_list:
return ("", base_filename, 0, 0, "Error: No prompts provided")
# Parse suffixes if provided
suffix_list = [s.strip() for s in suffixes.strip().split('\n') if s.strip()] if suffixes else []
total_count = len(prompt_list)
# Initialize or get state for this workflow
state_key = f"{workflow_id}_dynamic"
if state_key not in ITERATOR_STATE:
ITERATOR_STATE[state_key] = {
"index": 0,
"iteration": 0,
"random_order": list(range(total_count)),
"base_seed": generation_seed if generation_seed >= 0 else random.randint(0, 2147483647),
"current_seed": generation_seed if generation_seed >= 0 else random.randint(0, 2147483647)
}
state = ITERATOR_STATE[state_key]
# Handle reset
if reset:
state["index"] = 0
state["iteration"] = 0
state["random_order"] = list(range(total_count))
if generation_seed >= 0:
state["base_seed"] = generation_seed
state["current_seed"] = generation_seed
else:
state["base_seed"] = random.randint(0, 2147483647)
state["current_seed"] = state["base_seed"]
# Update random order if needed for prompt shuffling
if mode == "random":
random.shuffle(state["random_order"])
# Determine current index based on mode
if mode == "manual":
current_index = max(0, min(manual_index, total_count - 1))
elif mode == "single":
current_index = 0
elif mode == "random":
current_index = state["random_order"][state["index"]]
# Advance for next run
state["index"] = (state["index"] + 1) % total_count
if state["index"] == 0:
state["iteration"] += 1
else: # sequential
current_index = state["index"]
# Advance for next run
state["index"] = (state["index"] + 1) % total_count
if state["index"] == 0:
state["iteration"] += 1
# Get current prompt
current_prompt = prompt_list[current_index]
# Generate filename based on mode
if filename_mode == "suffix_list" and suffix_list:
suffix = suffix_list[current_index] if current_index < len(suffix_list) else f"_{current_index:03d}"
current_filename = f"{base_filename}{suffix}"
elif filename_mode == "template":
suffix = suffix_list[current_index] if current_index < len(suffix_list) else ""
current_filename = filename_template.format(
base=base_filename,
index=current_index,
suffix=suffix.lstrip('_')
)
else: # auto_index
current_filename = f"{base_filename}_{current_index:03d}"
# Handle seed generation based on mode
output_seed = state["current_seed"]
# Determine when to increment seed
should_increment = False
if seed_mode == "increment_prompt":
# Increment on every prompt
should_increment = True
elif seed_mode == "increment_batch" and current_index == 0 and state["iteration"] > 0:
# Increment only when starting a new batch
should_increment = True
elif seed_mode == "random":
# Always randomize
output_seed = random.randint(0, 2147483647)
# Apply increment if needed
if should_increment and seed_mode != "random":
state["current_seed"] = (state["current_seed"] + 1) % 2147483648
output_seed = state["current_seed"]
# Status message
status = f"Prompt {current_index + 1}/{total_count}"
if mode == "sequential":
status += f" (Iteration {state['iteration'] + 1})"
elif mode == "random":
status += " (random)"
return (current_prompt, current_filename, current_index, total_count, status, output_seed)
class PromptIterator:
"""
Basic prompt iterator node that cycles through a list of prompts
and generates corresponding filenames
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompts": ("STRING", {
"multiline": True,
"default": "prompt 1\nprompt 2\nprompt 3",
"dynamicPrompts": False
}),
"mode": (["sequential", "manual", "single"], {
"default": "sequential"
}),
"base_filename": ("STRING", {
"default": "output",
"multiline": False
}),
},
"optional": {
"filenames": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": False
}),
"manual_index": ("INT", {
"default": 0,
"min": 0,
"max": 9999,
"step": 1
}),
"reset": ("BOOLEAN", {
"default": False,
"label_on": "Reset",
"label_off": "Continue"
}),
"workflow_id": ("STRING", {
"default": "default",
"multiline": False
}),
}
}
RETURN_TYPES = ("STRING", "STRING", "INT", "INT", "STRING")
RETURN_NAMES = ("prompt", "filename", "current_index", "total_count", "status")
FUNCTION = "iterate_prompt"
CATEGORY = "utils/prompt"
OUTPUT_NODE = False
@classmethod
def IS_CHANGED(cls, **kwargs):
"""Force re-execution when inputs change"""
mode = kwargs.get("mode", "sequential")
if mode == "sequential":
# In sequential mode, always re-execute to advance
return float("NaN")
return False
def iterate_prompt(self, prompts: str, mode: str, base_filename: str,
filenames: str = "", manual_index: int = 0,
reset: bool = False, workflow_id: str = "default") -> Tuple:
"""
Main execution function for prompt iteration
"""
global ITERATOR_STATE
# Parse prompts and filenames
prompt_list = [p.strip() for p in prompts.strip().split('\n') if p.strip()]
filename_list = [f.strip() for f in filenames.strip().split('\n') if f.strip()] if filenames else []
if not prompt_list:
return ("", base_filename, 0, 0, "Error: No prompts provided")
total_count = len(prompt_list)
# Initialize or get state for this workflow
if workflow_id not in ITERATOR_STATE:
ITERATOR_STATE[workflow_id] = {"index": 0, "iteration": 0}
state = ITERATOR_STATE[workflow_id]
# Handle reset
if reset:
state["index"] = 0
state["iteration"] = 0
# Determine current index based on mode
if mode == "manual":
current_index = max(0, min(manual_index, total_count - 1))
elif mode == "single":
# Single mode: always use first prompt
current_index = 0
else: # sequential
current_index = state["index"]
# Advance for next run
state["index"] = (state["index"] + 1) % total_count
if state["index"] == 0:
state["iteration"] += 1
# Get current prompt
current_prompt = prompt_list[current_index]
# Generate filename
if filename_list and current_index < len(filename_list):
# Use provided filename
current_filename = filename_list[current_index]
else:
# Generate filename with index
current_filename = f"{base_filename}_{current_index:03d}"
# Status message
status = f"Prompt {current_index + 1}/{total_count}"
if mode == "sequential":
status += f" (Iteration {state['iteration'] + 1})"
return (current_prompt, current_filename, current_index, total_count, status)
class PromptIteratorAdvanced:
"""
Advanced prompt iterator with additional features like templates,
suffix lists, and more control options
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompts": ("STRING", {
"multiline": True,
"default": "face only headshot, facing camera directly\nLeft profile View - rotate face 90 degrees left\nRight profile View - rotate face 90 degrees right\nBack View - direct back of the head",
"dynamicPrompts": False
}),
"mode": (["sequential", "manual", "random", "single"], {
"default": "sequential"
}),
"filename_mode": (["list", "suffix_list", "template", "index"], {
"default": "suffix_list"
}),
"base_filename": ("STRING", {
"default": "character",
"multiline": False
}),
},
"optional": {
"filenames": ("STRING", {
"multiline": True,
"default": "",
"dynamicPrompts": False,
"placeholder": "One filename per line (for 'list' mode)"
}),
"suffixes": ("STRING", {
"multiline": True,
"default": "_front\n_left\n_right\n_back",
"dynamicPrompts": False,
"placeholder": "One suffix per line (for 'suffix_list' mode)"
}),
"filename_template": ("STRING", {
"default": "{base}_{index:03d}_{suffix}",
"multiline": False,
"placeholder": "Template with {base}, {index}, {suffix} placeholders"
}),
"prepend_text": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "Text to add before each prompt"
}),
"append_text": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "Text to add after each prompt"
}),
"manual_index": ("INT", {
"default": 0,
"min": 0,
"max": 9999,
"step": 1
}),
"loop_mode": (["once", "loop", "ping_pong"], {
"default": "loop"
}),
"reset": ("BOOLEAN", {
"default": False,
"label_on": "Reset",
"label_off": "Continue"
}),
"generation_seed": ("INT", {
"default": -1,
"min": -1,
"max": 2147483647,
"step": 1,
"display": "number"
}),
"seed_mode": (["fixed", "increment_batch", "increment_prompt", "random"], {
"default": "increment_batch"
}),
"workflow_id": ("STRING", {
"default": "default",
"multiline": False
}),
}
}
RETURN_TYPES = ("STRING", "STRING", "INT", "INT", "STRING", "INT", "STRING")
RETURN_NAMES = ("prompt", "filename", "current_index", "total_count", "status", "seed", "debug_info")
FUNCTION = "iterate_prompt_advanced"
CATEGORY = "utils/prompt"
OUTPUT_NODE = False
@classmethod
def IS_CHANGED(cls, **kwargs):
"""Force re-execution when needed"""
mode = kwargs.get("mode", "sequential")
if mode in ["sequential", "random"]:
return float("NaN")
return False
def iterate_prompt_advanced(self, prompts: str, mode: str, filename_mode: str,
base_filename: str, filenames: str = "",
suffixes: str = "", filename_template: str = "",
prepend_text: str = "", append_text: str = "",
manual_index: int = 0, loop_mode: str = "loop",
reset: bool = False, generation_seed: int = -1,
seed_mode: str = "increment_batch",
workflow_id: str = "default") -> Tuple:
"""
Advanced prompt iteration with enhanced features
"""
global ITERATOR_STATE
# Parse inputs
prompt_list = [p.strip() for p in prompts.strip().split('\n') if p.strip()]
filename_list = [f.strip() for f in filenames.strip().split('\n') if f.strip()] if filenames else []
suffix_list = [s.strip() for s in suffixes.strip().split('\n') if s.strip()] if suffixes else []
if not prompt_list:
return ("", base_filename, 0, 0, "Error: No prompts provided", "")
total_count = len(prompt_list)
# Initialize or get state
state_key = f"{workflow_id}_advanced"
if state_key not in ITERATOR_STATE:
ITERATOR_STATE[state_key] = {
"index": 0,
"iteration": 0,
"direction": 1, # For ping-pong mode
"random_order": list(range(total_count)),
"base_seed": generation_seed if generation_seed >= 0 else random.randint(0, 2147483647),
"current_seed": generation_seed if generation_seed >= 0 else random.randint(0, 2147483647)
}
state = ITERATOR_STATE[state_key]
# Handle reset
if reset:
state["index"] = 0
state["iteration"] = 0
state["direction"] = 1
state["random_order"] = list(range(total_count))
if generation_seed >= 0:
state["base_seed"] = generation_seed
state["current_seed"] = generation_seed
else:
state["base_seed"] = random.randint(0, 2147483647)
state["current_seed"] = state["base_seed"]
# Update random order if needed for prompt shuffling
if mode == "random":
random.shuffle(state["random_order"])
# Determine current index
if mode == "manual":
current_index = max(0, min(manual_index, total_count - 1))
elif mode == "single":
current_index = 0
elif mode == "random":
current_index = state["random_order"][state["index"]]
# Advance for next run
state["index"] = (state["index"] + 1) % total_count
if state["index"] == 0:
state["iteration"] += 1
else: # sequential
current_index = state["index"]
# Handle loop modes
if loop_mode == "once":
if state["index"] < total_count - 1:
state["index"] += 1
elif loop_mode == "ping_pong":
state["index"] += state["direction"]
if state["index"] >= total_count - 1:
state["direction"] = -1
state["index"] = total_count - 1
elif state["index"] <= 0:
state["direction"] = 1
state["index"] = 0
state["iteration"] += 1
else: # loop
state["index"] = (state["index"] + 1) % total_count
if state["index"] == 0:
state["iteration"] += 1
# Build prompt with prepend/append
base_prompt = prompt_list[current_index]
current_prompt = f"{prepend_text}{base_prompt}{append_text}".strip()
# Generate filename based on mode
if filename_mode == "list" and filename_list and current_index < len(filename_list):
current_filename = filename_list[current_index]
elif filename_mode == "suffix_list" and suffix_list:
suffix = suffix_list[current_index] if current_index < len(suffix_list) else f"_{current_index:03d}"
current_filename = f"{base_filename}{suffix}"
elif filename_mode == "template":
suffix = suffix_list[current_index] if current_index < len(suffix_list) else ""
current_filename = filename_template.format(
base=base_filename,
index=current_index,
suffix=suffix.lstrip('_') # Remove leading underscore if present
)
else: # index mode
current_filename = f"{base_filename}_{current_index:03d}"
# Build status
status = f"Prompt {current_index + 1}/{total_count}"
if mode == "sequential":
status += f" | Iteration {state['iteration'] + 1}"
if loop_mode == "ping_pong":
status += " (ping-pong)"
elif mode == "random":
status += " (random)"
# Handle seed generation based on mode
output_seed = state["current_seed"]
# Determine if we should increment the seed
should_increment = False
if seed_mode == "increment_prompt":
should_increment = True
elif seed_mode == "increment_batch" and current_index == 0 and state["iteration"] > 0:
should_increment = True
elif seed_mode == "random":
output_seed = random.randint(0, 2147483647)
state["current_seed"] = output_seed
# Apply increment if needed
if should_increment and seed_mode != "random":
state["current_seed"] = (state["current_seed"] + 1) % 2147483648
output_seed = state["current_seed"]
# Debug info
debug_info = json.dumps({
"mode": mode,
"filename_mode": filename_mode,
"current_index": current_index,
"state_index": state["index"],
"iteration": state["iteration"],
"loop_mode": loop_mode,
"filename": current_filename,
"seed": output_seed,
"seed_mode": seed_mode
}, indent=2)
return (current_prompt, current_filename, current_index, total_count, status, output_seed)
# Node registration
NODE_CLASS_MAPPINGS = {
"PromptIteratorDynamic": PromptIteratorDynamic,
"PromptIterator": PromptIterator,
"PromptIteratorAdvanced": PromptIteratorAdvanced,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PromptIteratorDynamic": "Prompt Iterator (Dynamic Inputs)",
"PromptIterator": "Prompt Iterator",
"PromptIteratorAdvanced": "Prompt Iterator (Advanced)",
}