From 3e18cfcb9fe52489f7efea4b2d7eeb89b223e3c3 Mon Sep 17 00:00:00 2001 From: Slava Cherkasov Date: Tue, 6 May 2025 17:48:20 +0200 Subject: [PATCH] added random syntax for prompt: {A | B | C} --- js/SmartPrompt.js | 8 +++-- nodes.py | 79 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/js/SmartPrompt.js b/js/SmartPrompt.js index b49e1fa..1e46c7b 100644 --- a/js/SmartPrompt.js +++ b/js/SmartPrompt.js @@ -46,16 +46,20 @@ function colorizeText(style, text) { function styleDefaultText(text) { const defaultTextStyle = "color: rgb(200, 200, 200);"; // Your default grey const punctuationStyle = "color: black;"; // Style for punctuation + const pipeStyle = "color: red;"; // Style for vertical line / braces let resultHtml = ''; // Iterate character by character for (let i = 0; i < text.length; i++) { const char = text[i]; - if (['.', ',', '(', ')', ';', ':'].includes(char)) { + if (['.', ',', '(', ')', ';', ':'].includes(char)) { // Removed { and } resultHtml += colorizeText(punctuationStyle, char); + } else if (['|', '{', '}'].includes(char)) { // Added { and } + resultHtml += colorizeText(pipeStyle, char); } else { // Optimization: Group consecutive non-punctuation characters let nonPunctSubstring = char; - while (i + 1 < text.length && !['.', ',', '(', ')', ';', ':'].includes(text[i + 1])) { + // Keep the while loop condition as is, it correctly handles braces and pipe + while (i + 1 < text.length && !['.', ',', '(', ')', ';', ':', '{', '}', '|'].includes(text[i + 1])) { i++; nonPunctSubstring += text[i]; } diff --git a/nodes.py b/nodes.py index 2b24180..d8a0d53 100644 --- a/nodes.py +++ b/nodes.py @@ -4,6 +4,9 @@ import comfy import comfy.sd # Added import for CLIP type access import json import sys +import re # Added for regex operations +import random # Added for random choice +import time # Added for IS_CHANGED # Custom Nodes for ComfyUI # Note: All nodes in this file should use the "Smart" prefix in their names @@ -439,14 +442,15 @@ class SmartLoadLoRA: class SmartPrompt: """ - A node that provides a multiline text input, removes comments, and optionally encodes it using a CLIP model. + A node that provides a multiline text input, removes comments, supports random selection syntax { A | B | C }, + and optionally encodes it using a CLIP model. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "text": ("STRING", {"multiline": True, "default": "", "tooltip": "Enter your prompt text. Lines with // comments will be cleaned up."}), + "text": ("STRING", {"multiline": True, "default": "", "tooltip": "Enter your prompt text. Lines with // comments will be cleaned up. Use { A | B | C } for random selection."}), }, "optional": { "clip": ("CLIP", {"tooltip": "Optional CLIP model for encoding the text."}) @@ -458,30 +462,83 @@ class SmartPrompt: FUNCTION = "process_prompt" CATEGORY = "SmartHelperNodes" + @staticmethod + def parse_random_syntax(text): + """Parses the { A | B | C } syntax and selects a random element.""" + def replace_match(match): + # Extract content inside braces and split by '|' + options = [opt.strip() for opt in match.group(1).split('|')] + # Return a randomly chosen option + return random.choice(options) + + # Regex to find { content } where content contains at least one | + # Example: {cat|dog|bird} + pattern = r'\{([^}]+?\|[^}]+?)\}' + + # Repeatedly apply substitution until no more matches are found + # This handles multiple occurrences, e.g., "{a|b} and {c|d}" + processed_text = text + # Limit iterations to prevent potential infinite loops with malformed input, though unlikely with this pattern + iterations = 0 + max_iterations = 100 # Safety limit + while re.search(pattern, processed_text) and iterations < max_iterations: + # Replace one occurrence at a time + processed_text = re.sub(pattern, replace_match, processed_text, count=1) + iterations += 1 + + if iterations >= max_iterations: + print(f"Warning: Max iterations reached during random syntax processing in SmartPrompt. Input text: {text}") + + return processed_text + def process_prompt(self, text, clip=None): - # Split text into lines, remove comment portions, and rejoin + # 1. Remove comments lines = text.splitlines() processed_lines = [] for line in lines: - # Find the position of // if it exists comment_pos = line.find('//') if comment_pos != -1: - # Keep only the part before the comment line = line[:comment_pos].rstrip() - # Only add non-empty lines after comment removal + # Keep lines that are not empty after comment removal if line.strip(): processed_lines.append(line) - processed_text = "\n".join(processed_lines) + comment_cleaned_text = "\n".join(processed_lines) + # 2. Process random syntax { A | B | C } + processed_text = self.parse_random_syntax(comment_cleaned_text) + + # 3. Encode if CLIP is provided conditioning = None if clip is not None: - # Based on CLIPTextEncode example tokens = clip.tokenize(processed_text) - conditioning = clip.encode_from_tokens_scheduled(tokens) - + # Use encode_from_tokens_scheduled if available, otherwise fall back to encode + if hasattr(clip, 'encode_from_tokens_scheduled'): + conditioning = clip.encode_from_tokens_scheduled(tokens) + elif hasattr(clip, 'encode'): + # Standard CLIPTextEncode uses encode + conditioning, _ = clip.encode_token_weights(tokens) # Assuming encode returns pooled_output as second value + else: + print(f"Warning: Provided CLIP object for SmartPrompt node is missing a recognized encoding method (encode or encode_from_tokens_scheduled).") + # Return empty conditioning or handle appropriately + # For now, returning None which might cause downstream issues if conditioning is expected. + # A more robust solution might involve returning a default "empty" conditioning object if possible. + return (processed_text, conditioning) - + + @classmethod + def IS_CHANGED(cls, text, clip=None, **kwargs): + # Check if the random syntax { A | B } is present in the input text + pattern = r'\{([^}]+?\|[^}]+?)\}' + # Use time.time() to force re-execution if the random syntax is detected + # Otherwise, rely on the hash of the text for standard change detection. + if re.search(pattern, text): + return time.time() + # Hash the input text to detect changes. Add clip object's potential changes if needed. + # Note: Hashing the clip object itself might be complex/unreliable. + # Relying on text changes is standard for prompt nodes. + return hash(text) + class SmartModelOrLoraToString: @classmethod def INPUT_TYPES(s):