Initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Ignore Python bytecode and cache directories
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
# Import all independent node files safely
|
||||
from .console_log import ConsoleLogNode
|
||||
from .execution_delay import ExecutionDelayNode
|
||||
from .file_loader import WildcardFileLoaderNode
|
||||
from .randomizer import MultilineRandomChanceNode
|
||||
from .text_concatenator import TextConcatenator4Node, TextConcatenator8Node
|
||||
from .text_tag_replacer import TextRandomReplacerNode
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"MultilineRandomChanceNode": MultilineRandomChanceNode,
|
||||
"ConsoleLogNode": ConsoleLogNode,
|
||||
"TextRandomReplacerNode": TextRandomReplacerNode,
|
||||
"WildcardFileLoaderNode": WildcardFileLoaderNode,
|
||||
"ExecutionDelayNode": ExecutionDelayNode,
|
||||
"TextConcatenator4Node": TextConcatenator4Node,
|
||||
"TextConcatenator8Node": TextConcatenator8Node, # New mapping
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"MultilineRandomChanceNode": "🎲 Weighted Multiline Randomizer",
|
||||
"ConsoleLogNode": "🖥️ Log to Console",
|
||||
"TextRandomReplacerNode": "🏷️ Text Random Replacer",
|
||||
"WildcardFileLoaderNode": "📂 Wildcard File Loader",
|
||||
"ExecutionDelayNode": "⏳ Execution Delay",
|
||||
"TextConcatenator4Node": "🧩 Text Concatenator (4-Way)",
|
||||
"TextConcatenator8Node": "🧱 Text Concatenator (8-Way)", # New visual display label
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
@@ -0,0 +1,32 @@
|
||||
class ConsoleLogNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"forceInput": True}),
|
||||
"prefix_label": ("STRING", {"default": "LOG TEXT"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("text",)
|
||||
FUNCTION = "log_text_to_terminal"
|
||||
CATEGORY = "TextRandomizer/Debug"
|
||||
|
||||
def log_text_to_terminal(self, text, prefix_label):
|
||||
cyan_start, yellow_start, color_reset = "\033[96m", "\033[93m", "\033[0m"
|
||||
|
||||
# Check if the incoming data is a batch list or a single string
|
||||
if isinstance(text, list):
|
||||
print(
|
||||
f"\n{cyan_start}[ℹ️ {prefix_label} - BATCH COUNT: {len(text)}]{color_reset}"
|
||||
)
|
||||
for idx, item in enumerate(text):
|
||||
print(f'{cyan_start} [{idx + 1}]:{yellow_start} "{item}"{color_reset}')
|
||||
print("")
|
||||
else:
|
||||
# Fallback for standard single string inputs
|
||||
print(f"\n{cyan_start}[ℹ️ {prefix_label}]{color_reset}")
|
||||
print(f'{yellow_start}"{text}"{color_reset}\n')
|
||||
|
||||
return (text,)
|
||||
@@ -0,0 +1,48 @@
|
||||
import time
|
||||
|
||||
|
||||
class ExecutionDelayNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
# The data flowing through the chain (Any type placeholder '*')
|
||||
"any_input": ("*", {"forceInput": True}),
|
||||
# The number of seconds to sleep/throttle
|
||||
"seconds_to_wait": (
|
||||
"INT",
|
||||
{"default": 5, "min": 0, "max": 3600, "step": 1},
|
||||
),
|
||||
# Visual label toggle to trace wait status in your terminal window
|
||||
"print_to_console": (["enable", "disable"], {"default": "enable"}),
|
||||
}
|
||||
}
|
||||
|
||||
# By declaring '*' as the output type, ComfyUI dynamically copies
|
||||
# the exact data type that entered the node (String, Latent, Image, etc.)
|
||||
RETURN_TYPES = ("*",)
|
||||
RETURN_NAMES = ("output_data",)
|
||||
FUNCTION = "trigger_throttle_delay"
|
||||
CATEGORY = "TextRandomizer/Utility"
|
||||
|
||||
def trigger_throttle_delay(self, any_input, seconds_to_wait, print_to_console):
|
||||
if seconds_to_wait > 0:
|
||||
if print_to_console == "enable":
|
||||
cyan_start, yellow_start, color_reset = (
|
||||
"\033[96m",
|
||||
"\033[93m",
|
||||
"\033[0m",
|
||||
)
|
||||
print(
|
||||
f"{cyan_start}[⏳ Throttle]{color_reset} Sleeping for {yellow_start}{seconds_to_wait}{color_reset} seconds..."
|
||||
)
|
||||
|
||||
# Freeze execution thread entirely
|
||||
time.sleep(seconds_to_wait)
|
||||
|
||||
if print_to_console == "enable":
|
||||
print(
|
||||
f"{cyan_start}[⏳ Throttle]{color_reset} Resume signal sent down pipeline."
|
||||
)
|
||||
|
||||
return (any_input,)
|
||||
@@ -0,0 +1,79 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
from .randomizer import MultilineRandomChanceNode
|
||||
|
||||
|
||||
class WildcardFileLoaderNode:
|
||||
@classmethod
|
||||
def get_wildcard_files(cls):
|
||||
"""Scans the wildcards directory recursively to find all text files, preserving folder structures."""
|
||||
base_dir = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "wildcards")
|
||||
)
|
||||
os.makedirs(base_dir, exist_ok=True)
|
||||
|
||||
file_list = []
|
||||
for root, _, files in os.walk(base_dir):
|
||||
for file in files:
|
||||
if file.endswith(".txt"):
|
||||
full_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(full_path, base_dir)
|
||||
file_list.append(rel_path.replace("\\", "/"))
|
||||
|
||||
if not file_list:
|
||||
return ["No .txt files found in ComfyUI/wildcards/"]
|
||||
|
||||
return sorted(file_list)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
file_list = cls.get_wildcard_files()
|
||||
return {
|
||||
"required": {
|
||||
"wildcard_file": (file_list,),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("selected_text",)
|
||||
FUNCTION = "load_and_randomize_file"
|
||||
CATEGORY = "TextRandomizer/Text"
|
||||
|
||||
def load_and_randomize_file(self, wildcard_file, seed):
|
||||
base_dir = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "wildcards")
|
||||
)
|
||||
file_path = os.path.join(base_dir, wildcard_file)
|
||||
|
||||
if not os.path.exists(file_path) or wildcard_file.startswith("No .txt files"):
|
||||
return ("",)
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
raw_lines = f.readlines()
|
||||
except Exception:
|
||||
return ("",)
|
||||
|
||||
# Filter out comments and blank lines
|
||||
clean_text_block = "\n".join(
|
||||
[
|
||||
line.strip()
|
||||
for line in raw_lines
|
||||
if line.strip() and not line.startswith("#")
|
||||
]
|
||||
)
|
||||
|
||||
if not clean_text_block:
|
||||
return ("",)
|
||||
|
||||
# 1. Fetch the raw tuple output generated by the randomizer module code
|
||||
randomizer = MultilineRandomChanceNode()
|
||||
final_string_tuple = randomizer.process_random_text(clean_text_block, seed)
|
||||
|
||||
# 2. Extract index 0 to strip away the internal tuple container structure
|
||||
pure_extracted_string = final_string_tuple[0]
|
||||
|
||||
# 3. Return the pure string packaged cleanly inside ComfyUI's mandatory outer node tuple wrapper
|
||||
return (pure_extracted_string,)
|
||||
@@ -0,0 +1,77 @@
|
||||
import random
|
||||
import re
|
||||
|
||||
|
||||
class MultilineRandomChanceNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"default": "A professional photo of a {female::5|male} mage, casting a {fire::5|ice::2|lightning} spell, cinematic lighting.",
|
||||
},
|
||||
),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
|
||||
},
|
||||
"optional": {
|
||||
"regex_pattern": ("STRING", {"default": ",\\s*,"}),
|
||||
"regex_replacement": ("STRING", {"default": ","}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("selected_text",)
|
||||
FUNCTION = "process_random_text"
|
||||
CATEGORY = "TextRandomizer/Text"
|
||||
|
||||
def _parse_weights(self, options_list, separator="::"):
|
||||
choices, weights = [], []
|
||||
for opt in options_list:
|
||||
if separator in opt:
|
||||
parts = opt.rsplit(separator, 1)
|
||||
choice_text = parts[0].strip()
|
||||
try:
|
||||
weight = float(parts[1].strip())
|
||||
except ValueError:
|
||||
weight = 1.0
|
||||
else:
|
||||
choice_text = opt.strip()
|
||||
weight = 1.0
|
||||
choices.append(choice_text)
|
||||
weights.append(weight)
|
||||
return choices, weights
|
||||
|
||||
def _replace_brackets(self, match):
|
||||
content = match.group(1)
|
||||
raw_options = content.split("|")
|
||||
choices, weights = self._parse_weights(raw_options, separator="::")
|
||||
if choices:
|
||||
return random.choices(choices, weights=weights, k=1)[0]
|
||||
return ""
|
||||
|
||||
def process_random_text(self, text, seed, regex_pattern="", regex_replacement=""):
|
||||
random.seed(seed)
|
||||
lines = [line.strip() for line in text.split("\n") if line.strip()]
|
||||
if not lines:
|
||||
return ("",)
|
||||
|
||||
base_choices, base_weights = self._parse_weights(lines, separator="::")
|
||||
selected_line = random.choices(base_choices, weights=base_weights, k=1)[0]
|
||||
|
||||
bracket_pattern = re.compile(r"\{([^{}]+)\}")
|
||||
loop_guard = 0
|
||||
while bracket_pattern.search(selected_line) and loop_guard < 10:
|
||||
selected_line = bracket_pattern.sub(self._replace_brackets, selected_line)
|
||||
loop_guard += 1
|
||||
|
||||
if regex_pattern:
|
||||
try:
|
||||
compiled_regex = re.compile(regex_pattern)
|
||||
selected_line = compiled_regex.sub(regex_replacement, selected_line)
|
||||
except re.error:
|
||||
pass
|
||||
|
||||
return (selected_line,)
|
||||
@@ -0,0 +1,113 @@
|
||||
import re
|
||||
|
||||
|
||||
class TextConcatenator4Node:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"separator": ("STRING", {"default": "; "}),
|
||||
},
|
||||
"optional": {
|
||||
"input_1": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_2": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_3": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_4": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("merged_text",)
|
||||
FUNCTION = "combine_strings"
|
||||
CATEGORY = "TextRandomizer/Text"
|
||||
|
||||
def combine_strings(
|
||||
self, separator, input_1="", input_2="", input_3="", input_4=""
|
||||
):
|
||||
raw_fragments = [input_1, input_2, input_3, input_4]
|
||||
active_fragments = []
|
||||
|
||||
for frag in raw_fragments:
|
||||
if frag is not None:
|
||||
clean_frag = str(frag).strip()
|
||||
if clean_frag:
|
||||
active_fragments.append(clean_frag)
|
||||
|
||||
if not active_fragments:
|
||||
return ("",)
|
||||
|
||||
combined_output = separator.join(active_fragments)
|
||||
combined_output = re.sub(r"\s+", " ", combined_output)
|
||||
combined_output = re.sub(r",\s*,", ", ", combined_output)
|
||||
combined_output = combined_output.strip().rstrip(",")
|
||||
|
||||
return (combined_output.strip(),)
|
||||
|
||||
|
||||
class TextConcatenator8Node:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"separator": ("STRING", {"default": "; "}),
|
||||
},
|
||||
"optional": {
|
||||
# 8 dedicated, predictable slots accepting typing OR wire inputs
|
||||
"input_1": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_2": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_3": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_4": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_5": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_6": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_7": ("STRING", {"multiline": True, "default": ""}),
|
||||
"input_8": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("merged_text",)
|
||||
FUNCTION = "combine_strings"
|
||||
CATEGORY = "TextRandomizer/Text"
|
||||
|
||||
def combine_strings(
|
||||
self,
|
||||
separator,
|
||||
input_1="",
|
||||
input_2="",
|
||||
input_3="",
|
||||
input_4="",
|
||||
input_5="",
|
||||
input_6="",
|
||||
input_7="",
|
||||
input_8="",
|
||||
):
|
||||
# 1. Gather all 8 potential input strings
|
||||
raw_fragments = [
|
||||
input_1,
|
||||
input_2,
|
||||
input_3,
|
||||
input_4,
|
||||
input_5,
|
||||
input_6,
|
||||
input_7,
|
||||
input_8,
|
||||
]
|
||||
active_fragments = []
|
||||
|
||||
# 2. Iterate and drop empty text values or skipped wires safely
|
||||
for frag in raw_fragments:
|
||||
if frag is not None:
|
||||
clean_frag = str(frag).strip()
|
||||
if clean_frag:
|
||||
active_fragments.append(clean_frag)
|
||||
|
||||
if not active_fragments:
|
||||
return ("",)
|
||||
|
||||
# 3. Assemble and apply standard punctuation/spacing cleanup passes
|
||||
combined_output = separator.join(active_fragments)
|
||||
combined_output = re.sub(r"\s+", " ", combined_output)
|
||||
combined_output = re.sub(r",\s*,", ", ", combined_output)
|
||||
combined_output = combined_output.strip().rstrip(",")
|
||||
|
||||
return (combined_output.strip(),)
|
||||
@@ -0,0 +1,98 @@
|
||||
import re
|
||||
|
||||
from .randomizer import MultilineRandomChanceNode
|
||||
|
||||
|
||||
class TextRandomReplacerNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
# "default": "A {professional photo|2d sketch} of a #CHARACTER in a #LOCATION, cinematic lighting.",
|
||||
},
|
||||
),
|
||||
#"target_tag": ("STRING", {"default": "#CHARACTER"}),
|
||||
"target_tag": ("STRING", {"default": "#TAG"}),
|
||||
"replacement_options": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
# "default": "ancient elven {wizard|mage}\ncyberpunk {hacker|netrunner} :: 2",
|
||||
},
|
||||
),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("completed_prompt",)
|
||||
FUNCTION = "replace_tag_with_random"
|
||||
CATEGORY = "TextRandomizer/Text"
|
||||
|
||||
def _process_single_string(
|
||||
self, single_text, target_tag, chosen_replacement, randomizer
|
||||
):
|
||||
"""Helper to swap the tag and expand any curly brackets in a single prompt string."""
|
||||
if target_tag and target_tag in single_text:
|
||||
text_with_tag_swapped = single_text.replace(target_tag, chosen_replacement)
|
||||
else:
|
||||
text_with_tag_swapped = single_text
|
||||
|
||||
bracket_pattern = re.compile(r"\{([^{}]+)\}")
|
||||
final_prompt = text_with_tag_swapped
|
||||
|
||||
loop_guard = 0
|
||||
while bracket_pattern.search(final_prompt) and loop_guard < 10:
|
||||
final_prompt = bracket_pattern.sub(
|
||||
randomizer._replace_brackets, final_prompt
|
||||
)
|
||||
loop_guard += 1
|
||||
|
||||
return final_prompt
|
||||
|
||||
def replace_tag_with_random(self, text, target_tag, replacement_options, seed):
|
||||
randomizer = MultilineRandomChanceNode()
|
||||
|
||||
# 1. Standardise the replacement options into a flat list of pure strings
|
||||
if isinstance(replacement_options, list):
|
||||
options_list = replacement_options
|
||||
else:
|
||||
# FIX: We unpack index [0] to extract the pure string from the randomizer tuple
|
||||
chosen_tuple = randomizer.process_random_text(replacement_options, seed)
|
||||
options_list = [chosen_tuple[0]]
|
||||
|
||||
# 2. Standardise the input template text into a list
|
||||
if isinstance(text, list):
|
||||
text_list = text
|
||||
else:
|
||||
text_list = [text]
|
||||
|
||||
final_batch_output = []
|
||||
|
||||
# 3. Zip or loop through the text templates and your batch of options
|
||||
max_batch = max(len(text_list), len(options_list))
|
||||
|
||||
for i in range(max_batch):
|
||||
single_text_item = text_list[i % len(text_list)]
|
||||
chosen_replacement = options_list[i % len(options_list)]
|
||||
|
||||
processed_item = self._process_single_string(
|
||||
single_text_item, target_tag, chosen_replacement, randomizer
|
||||
)
|
||||
final_batch_output.append(processed_item)
|
||||
|
||||
# 4. Clean up the output format for ComfyUI pipeline compliance
|
||||
if (
|
||||
len(final_batch_output) == 1
|
||||
and not isinstance(text, list)
|
||||
and not isinstance(replacement_options, list)
|
||||
):
|
||||
return (final_batch_output[0],)
|
||||
else:
|
||||
return (final_batch_output,)
|
||||
@@ -0,0 +1,26 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "CustomUtils.ReloadUIButton",
|
||||
|
||||
// 1. Define the actual browser execution task
|
||||
commands: [
|
||||
{
|
||||
id: "reloadComfyUICommand",
|
||||
label: "🔄 Reload ComfyUI",
|
||||
function: () => {
|
||||
// Instantly soft-reboots the user interface without losing local storage cache
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// 2. Inject the command into ComfyUI's main top utility bar menu layout
|
||||
menuCommands: [
|
||||
{
|
||||
// Places it under 'Menu -> Extensions -> Reload UI' or inside the Topbar depending on your active UI version
|
||||
path: ["Extensions"],
|
||||
commands: ["reloadComfyUICommand"]
|
||||
}
|
||||
]
|
||||
});
|
||||
Reference in New Issue
Block a user