Updated TextRandomReplacerNode and TextMultiLineRandomReplacerNode to

use BaseTextRandomReplacerNode, added a Log to Workspace to view the
generated prompts
This commit is contained in:
Eugene
2026-07-18 21:10:24 +02:00
parent 8b31505a81
commit c88a64135e
4 changed files with 136 additions and 91 deletions
+22 -6
View File
@@ -1,18 +1,29 @@
WEB_DIRECTORY = "./web"
import os
# Import all independent node files safely
from .console_log import ConsoleLogNode
from .console_log import ConsoleLogNode, LogToWorkspace, LogToWorkspaceAdv
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, TextMultiLineRandomReplacerNode
from .text_tag_replacer import TextRandomReplacerNode, TextMultiLineRandomReplacerNode, TextChainReplacerNode
# Get the exact absolute directory path of this __init__.py file
NODE_ROOT = os.path.dirname(os.path.abspath(__file__))
# Point ComfyUI safely to your web folder
WEB_DIRECTORY = os.path.join(NODE_ROOT, "web")
NODE_CLASS_MAPPINGS = {
"MultilineRandomChanceNode": MultilineRandomChanceNode,
"ConsoleLogNode": ConsoleLogNode,
"LogToWorkspace": LogToWorkspace,
"LogToWorkspaceAdv": LogToWorkspaceAdv,
"TextRandomReplacerNode": TextRandomReplacerNode,
"TextMultiLineRandomReplacerNode": TextMultiLineRandomReplacerNode,
"TextChainReplacerNode": TextChainReplacerNode,
"WildcardFileLoaderNode": WildcardFileLoaderNode,
"ExecutionDelayNode": ExecutionDelayNode,
"TextConcatenator4Node": TextConcatenator4Node,
@@ -21,13 +32,18 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"MultilineRandomChanceNode": "🎲 Weighted Multiline Randomizer",
"ConsoleLogNode": "🖥️ Log to Console",
"TextRandomReplacerNode": "🏷️ Text Random Replacer",
"TextMultiLineRandomReplacerNode": "🏷️ Text Random Replacer (MultiLine)",
"TextChainReplacerNode": "🏷️ Text Random Replacer Chain",
"WildcardFileLoaderNode": "📂 Wildcard File Loader",
"ExecutionDelayNode": "⏳ Execution Delay",
"TextConcatenator4Node": "🧩 Text Concatenator (4-Way)",
"TextConcatenator8Node": "🧱 Text Concatenator (8-Way)", # New visual display label
"TextConcatenator8Node": "🧱 Text Concatenator (8-Way)",
"ConsoleLogNode": "🖥️ Log to Console",
"LogToWorkspace": "🖥️ Log to Workspace",
"LogToWorkspaceAdv": "🖥️ Log to Workspace Advanced",
"ExecutionDelayNode": "⏳ Execution Delay",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
+92
View File
@@ -1,3 +1,6 @@
import json
import re
class ConsoleLogNode:
@classmethod
def INPUT_TYPES(cls):
@@ -30,3 +33,92 @@ class ConsoleLogNode:
print(f'{yellow_start}"{text}"{color_reset}\n')
return (text,)
class LogToWorkspace:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"forceInput": True}), # Accepts any incoming string
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "display_text"
OUTPUT_NODE = True # Necessary so ComfyUI runs it even without downstream outputs
CATEGORY = "TextRandomizer/Debug"
def display_text(self, text):
# Normalize: ensure text is wrapped into a clean, uniform list structure
if isinstance(text, list):
ui_text_list = [str(item) for item in text]
# Convert list back to a string format for the node's standard execution pipeline output
result_text = "\n".join(ui_text_list)
else:
ui_text_list = [str(text)]
result_text = str(text)
# ComfyUI's UI engine expects a uniform array of strings to iterate over on the frontend canvas
return {"ui": {"text": ui_text_list}, "result": (result_text,)}
class LogToWorkspaceAdv:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
# Toggles to control clean up and sanitization pipelines
"remove_empty_commas": ("BOOLEAN", {"default": True}),
"remove_empty_lines": ("BOOLEAN", {"default": True}),
"collapse_spaces": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "display_text"
OUTPUT_NODE = True
CATEGORY = "TextRandomizer/Debug"
def display_text(self, text, remove_empty_commas, remove_empty_lines, collapse_spaces):
# 1. Standardize processing into a unified text block string
if isinstance(text, list):
raw_text = "\n".join([str(item) for item in text])
else:
raw_text = str(text)
# 2. RUN SANITIZATION FILTERS
# Scrub redundant, broken commas left behind by empty wildcards (e.g., ", ,", ",,")
if remove_empty_commas:
# Replaces multiple sequential commas (with optional spacing) with a single comma
raw_text = re.sub(r',\s*,+', ',', raw_text)
# Cleans up floating leading or trailing commas inside line strings
raw_text = re.sub(r'^\s*,\s*|\s*,\s*$', '', raw_text, flags=re.MULTILINE)
# Clean repeating whitespace blocks down to tight single space blocks
if collapse_spaces:
raw_text = re.sub(r'[ \t]+', ' ', raw_text)
# Remove ghost breaks or stacked blank line carriage returns
if remove_empty_lines:
# Splitting lines to inspect individual strings safely
lines = [line.strip() for line in raw_text.splitlines()]
# Filter away completely empty arrays or spacer elements
ui_text_list = [line for line in lines if line]
result_text = "\n".join(ui_text_list)
else:
ui_text_list = raw_text.splitlines()
result_text = raw_text
# Fallback safeguard layout value if everything is wiped clean
if not ui_text_list:
ui_text_list = [""]
result_text = ""
return {"ui": {"text": ui_text_list}, "result": (result_text,)}
-4
View File
@@ -37,8 +37,6 @@ class TextConcatenator4Node:
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(),)
@@ -106,8 +104,6 @@ class TextConcatenator8Node:
# 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(),)
+22 -81
View File
@@ -2,21 +2,16 @@ import re
from .randomizer import MultilineRandomChanceNode
class TextRandomReplacerNode:
class TextReplacerNodeBase:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": (
"STRING",
{
"multiline": False,
"default": "#TAG"
# "default": "A {professional photo|2d sketch} of a #CHARACTER in a #LOCATION, cinematic lighting.",
},
{"multiline": cls.MULTILINE, "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",
@@ -26,7 +21,7 @@ class TextRandomReplacerNode:
# "default": "ancient elven {wizard|mage}\ncyberpunk {hacker|netrunner} :: 2",
},
),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
"seed": ("INT", {"default": 1, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
}
}
@@ -98,20 +93,21 @@ class TextRandomReplacerNode:
return (final_batch_output,)
class TextMultiLineRandomReplacerNode:
class TextRandomReplacerNode(TextReplacerNodeBase):
MULTILINE = False
class TextMultiLineRandomReplacerNode(TextReplacerNodeBase):
MULTILINE = True
class TextChainReplacerNode(TextReplacerNodeBase):
@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"}),
"input_chain": ("STRING", {"multiline": False, "default": ""}),
"text": ("STRING", {"multiline": True, "default": ""}),
"target_tag": ("STRING", {"default": "#TAG"}),
"replacement_options": (
"STRING",
@@ -127,67 +123,12 @@ class TextMultiLineRandomReplacerNode:
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("completed_prompt",)
FUNCTION = "replace_tag_with_random"
FUNCTION = "chain_texts"
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,)
def chain_texts(self, input_chain, text, target_tag, replacement_options, seed):
"""Helper to concatenate the input chain with the new text and replace tags."""
# Process the input chain
processed_chain = self.replace_tag_with_random(text, target_tag, replacement_options, seed)
# Concatenate the processed chain and the new text
return (f"{input_chain} {processed_chain[0]}",)