From c88a64135e5b58e4afa6653ee41d4ce3fbfff432 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 18 Jul 2026 21:10:24 +0200 Subject: [PATCH] Updated TextRandomReplacerNode and TextMultiLineRandomReplacerNode to use BaseTextRandomReplacerNode, added a Log to Workspace to view the generated prompts --- __init__.py | 28 +++++++++--- console_log.py | 92 ++++++++++++++++++++++++++++++++++++++ text_concatenator.py | 4 -- text_tag_replacer.py | 103 +++++++++---------------------------------- 4 files changed, 136 insertions(+), 91 deletions(-) diff --git a/__init__.py b/__init__.py index 86479cf..8e0def0 100644 --- a/__init__.py +++ b/__init__.py @@ -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"] diff --git a/console_log.py b/console_log.py index 890e3a4..2602556 100644 --- a/console_log.py +++ b/console_log.py @@ -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,)} diff --git a/text_concatenator.py b/text_concatenator.py index 455a51f..c540822 100644 --- a/text_concatenator.py +++ b/text_concatenator.py @@ -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(),) diff --git a/text_tag_replacer.py b/text_tag_replacer.py index c264a00..067f1ff 100644 --- a/text_tag_replacer.py +++ b/text_tag_replacer.py @@ -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]}",)