Replace individual seed parameters with line_index system to provide more predictable wildcard selection. This aligns with TextFileLoader's behavior and allows both random selection (when line_index=-1) and specific line selection. The global_seed parameter is renamed to seed for clarity.
582 lines
22 KiB
Python
582 lines
22 KiB
Python
import os
|
|
import random
|
|
import time
|
|
from pathlib import Path
|
|
from dynamicprompts.generators import RandomPromptGenerator
|
|
|
|
# Cache to store text lines per file
|
|
text_file_cache = {}
|
|
# Dictionary to track last execution time for each node instance
|
|
last_execution_time = {}
|
|
|
|
class MultilineTextInput:
|
|
"""
|
|
A node that provides a multiline text input and outputs the entered text as a string.
|
|
Now supports dynamicprompts syntax for variants and wildcards.
|
|
"""
|
|
CATEGORY = "Animagine-Prompt"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"text": ("STRING", {
|
|
"multiline": True,
|
|
"default": "",
|
|
"placeholder": "Enter your text here..."
|
|
}),
|
|
"use_dynamic_prompts": ("BOOLEAN", {
|
|
"default": True,
|
|
"label": "Process Dynamic Prompts"
|
|
}),
|
|
"seed": ("INT", {
|
|
"default": -1,
|
|
"min": -1,
|
|
"max": 2**32 - 1,
|
|
"display": "number",
|
|
"tooltip": "Seed for dynamic prompts generation (-1 for random)"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("text",)
|
|
FUNCTION = "output_text"
|
|
|
|
def output_text(self, text, use_dynamic_prompts, seed):
|
|
"""
|
|
Processes and returns the text, using dynamicprompts if enabled.
|
|
|
|
Args:
|
|
text (str): Input text from the multiline input
|
|
use_dynamic_prompts (bool): Whether to process with dynamicprompts
|
|
seed (int): Seed for random generation, -1 for random seed
|
|
|
|
Returns:
|
|
tuple: Contains the processed text as a string
|
|
"""
|
|
if not use_dynamic_prompts:
|
|
return (text,)
|
|
|
|
try:
|
|
# Use current time as unique identifier for this execution
|
|
node_instance_id = id(self)
|
|
current_time = time.time()
|
|
last_execution_time[node_instance_id] = current_time
|
|
|
|
# Set up seed
|
|
if seed == -1:
|
|
actual_seed = random.randint(0, 2**32 - 1)
|
|
else:
|
|
actual_seed = seed
|
|
|
|
# Set random seed for reproducible results
|
|
random.seed(actual_seed)
|
|
|
|
# Process with dynamicprompts
|
|
generator = RandomPromptGenerator()
|
|
processed_text = generator.generate(text, num_images=1)[0]
|
|
|
|
# Reset random seed to avoid affecting other randomness
|
|
random.seed(None)
|
|
|
|
return (processed_text,)
|
|
except Exception as e:
|
|
return (f"Dynamic Prompts Error: {str(e)}\nOriginal text: {text}",)
|
|
|
|
|
|
class TextFileLoader:
|
|
"""
|
|
A node that loads a text file and returns a specific line or a random line.
|
|
Ensures random choices are refreshed on each workflow execution.
|
|
"""
|
|
CATEGORY = "Animagine-Prompt"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"file_path": ("STRING", {
|
|
"default": "",
|
|
"placeholder": "Path to text file"
|
|
}),
|
|
"line_index": ("INT", {
|
|
"default": -1,
|
|
"min": -1,
|
|
"display": "number",
|
|
"tooltip": "Index of line to select (-1 for random)"
|
|
}),
|
|
"seed": ("INT", {
|
|
"default": -1,
|
|
"min": -1,
|
|
"max": 2**32 - 1,
|
|
"display": "number",
|
|
"tooltip": "Seed for random selection (-1 for time-based seed)"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("selected_line",)
|
|
FUNCTION = "load_text_line"
|
|
|
|
def load_text_line(self, file_path, line_index, seed):
|
|
"""
|
|
Loads a text file and returns a specific line or a random line.
|
|
Ensures new random choices on each workflow execution.
|
|
|
|
Args:
|
|
file_path (str): Path to the text file
|
|
line_index (int): Index of the line to select, or -1 for random selection
|
|
seed (int): Seed for random selection, -1 for time-based seed
|
|
|
|
Returns:
|
|
tuple: Contains the selected line as a string
|
|
"""
|
|
try:
|
|
# Use current time as unique identifier for this execution
|
|
node_instance_id = id(self)
|
|
current_time = time.time()
|
|
last_execution_time[node_instance_id] = current_time
|
|
|
|
# Normalize path
|
|
file_path = os.path.normpath(file_path.strip())
|
|
|
|
# Check if file exists
|
|
if not os.path.isfile(file_path):
|
|
return (f"Error: File not found at {file_path}",)
|
|
|
|
# Get file modification time
|
|
file_mod_time = os.path.getmtime(file_path)
|
|
|
|
# Use cache if available and file hasn't been modified
|
|
cache_key = file_path
|
|
if cache_key in text_file_cache and text_file_cache[cache_key]["mod_time"] == file_mod_time:
|
|
lines = text_file_cache[cache_key]["lines"]
|
|
else:
|
|
# Read file and store in cache
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
lines = [line.rstrip() for line in f if line.strip()]
|
|
text_file_cache[cache_key] = {
|
|
"lines": lines,
|
|
"mod_time": file_mod_time
|
|
}
|
|
|
|
# Check if file is empty
|
|
if not lines:
|
|
return ("Error: File is empty",)
|
|
|
|
# Select line
|
|
if line_index == -1:
|
|
# Set up random seed for consistent but different choices
|
|
if seed == -1:
|
|
# Time-based seed ensures different choice on each run
|
|
random_seed = int(current_time * 1000) % (2**32)
|
|
else:
|
|
# User-provided seed for reproducible randomness
|
|
random_seed = seed
|
|
|
|
# Set the seed for this selection
|
|
random.seed(random_seed)
|
|
|
|
# Random selection
|
|
selected_line = random.choice(lines)
|
|
|
|
# Reset random seed to avoid affecting other randomness
|
|
random.seed(None)
|
|
else:
|
|
# Check if index is valid
|
|
if line_index < 0 or line_index >= len(lines):
|
|
return (f"Error: Line index {line_index} out of range (0-{len(lines)-1})",)
|
|
selected_line = lines[line_index]
|
|
|
|
return (selected_line,)
|
|
|
|
except Exception as e:
|
|
return (f"Error loading file: {str(e)}",)
|
|
|
|
|
|
class MultiWildcardLoader:
|
|
"""
|
|
A node that loads multiple wildcard files and concatenates their selected lines.
|
|
Supports template mode with placeholder syntax like {filename.txt}.
|
|
Uses line_index system like TextFileLoader for consistent selection.
|
|
"""
|
|
CATEGORY = "Animagine-Prompt"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"mode": (["slots", "template"], {
|
|
"default": "slots",
|
|
"tooltip": "Slots: Use individual wildcard slots. Template: Use template with {filename.txt} placeholders"
|
|
}),
|
|
"template_text": ("STRING", {
|
|
"multiline": True,
|
|
"default": "{expressions.txt} {poses.txt} wearing {clothing.txt} in a {scenarios.txt}",
|
|
"placeholder": "Template with {filename.txt} placeholders"
|
|
}),
|
|
"separator": ("STRING", {
|
|
"default": ", ",
|
|
"placeholder": "Separator between wildcards (slots mode only)"
|
|
}),
|
|
"seed": ("INT", {
|
|
"default": -1,
|
|
"min": -1,
|
|
"max": 2**32 - 1,
|
|
"display": "number",
|
|
"tooltip": "Seed for random selection (-1 for time-based seed)"
|
|
})
|
|
},
|
|
"optional": {
|
|
# Wildcard slot 1
|
|
"wildcard_1_path": ("STRING", {
|
|
"default": "",
|
|
"placeholder": "Path to first wildcard file"
|
|
}),
|
|
"wildcard_1_enabled": ("BOOLEAN", {
|
|
"default": True,
|
|
"label": "Enable Wildcard 1"
|
|
}),
|
|
"wildcard_1_line_index": ("INT", {
|
|
"default": -1,
|
|
"min": -1,
|
|
"display": "number",
|
|
"tooltip": "Line index for wildcard 1 (-1 for random)"
|
|
}),
|
|
|
|
# Wildcard slot 2
|
|
"wildcard_2_path": ("STRING", {
|
|
"default": "",
|
|
"placeholder": "Path to second wildcard file"
|
|
}),
|
|
"wildcard_2_enabled": ("BOOLEAN", {
|
|
"default": True,
|
|
"label": "Enable Wildcard 2"
|
|
}),
|
|
"wildcard_2_line_index": ("INT", {
|
|
"default": -1,
|
|
"min": -1,
|
|
"display": "number",
|
|
"tooltip": "Line index for wildcard 2 (-1 for random)"
|
|
}),
|
|
|
|
# Wildcard slot 3
|
|
"wildcard_3_path": ("STRING", {
|
|
"default": "",
|
|
"placeholder": "Path to third wildcard file"
|
|
}),
|
|
"wildcard_3_enabled": ("BOOLEAN", {
|
|
"default": True,
|
|
"label": "Enable Wildcard 3"
|
|
}),
|
|
"wildcard_3_line_index": ("INT", {
|
|
"default": -1,
|
|
"min": -1,
|
|
"display": "number",
|
|
"tooltip": "Line index for wildcard 3 (-1 for random)"
|
|
}),
|
|
|
|
# Wildcard slot 4
|
|
"wildcard_4_path": ("STRING", {
|
|
"default": "",
|
|
"placeholder": "Path to fourth wildcard file"
|
|
}),
|
|
"wildcard_4_enabled": ("BOOLEAN", {
|
|
"default": True,
|
|
"label": "Enable Wildcard 4"
|
|
}),
|
|
"wildcard_4_line_index": ("INT", {
|
|
"default": -1,
|
|
"min": -1,
|
|
"display": "number",
|
|
"tooltip": "Line index for wildcard 4 (-1 for random)"
|
|
}),
|
|
|
|
# Wildcard slot 5
|
|
"wildcard_5_path": ("STRING", {
|
|
"default": "",
|
|
"placeholder": "Path to fifth wildcard file"
|
|
}),
|
|
"wildcard_5_enabled": ("BOOLEAN", {
|
|
"default": True,
|
|
"label": "Enable Wildcard 5"
|
|
}),
|
|
"wildcard_5_line_index": ("INT", {
|
|
"default": -1,
|
|
"min": -1,
|
|
"display": "number",
|
|
"tooltip": "Line index for wildcard 5 (-1 for random)"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING")
|
|
RETURN_NAMES = ("combined_text", "preview_text")
|
|
FUNCTION = "load_multiple_wildcards"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(s, mode, template_text, separator, seed, **kwargs):
|
|
"""
|
|
This method tells ComfyUI when the node output should be recalculated.
|
|
It helps with real-time preview by making the output update when parameters change.
|
|
"""
|
|
# Create a hash of all input parameters to detect changes
|
|
import hashlib
|
|
|
|
# Combine all parameters into a string
|
|
params_str = f"{mode}_{template_text}_{separator}_{seed}"
|
|
|
|
# Add wildcard parameters
|
|
for i in range(1, 6):
|
|
path = kwargs.get(f"wildcard_{i}_path", "")
|
|
enabled = kwargs.get(f"wildcard_{i}_enabled", True)
|
|
line_index = kwargs.get(f"wildcard_{i}_line_index", -1)
|
|
params_str += f"_{path}_{enabled}_{line_index}"
|
|
|
|
# Return hash of parameters - when this changes, ComfyUI will recalculate
|
|
return hashlib.md5(params_str.encode()).hexdigest()
|
|
|
|
def _resolve_file_path(self, file_path):
|
|
"""
|
|
Resolve file path, trying relative to project directory first, then absolute.
|
|
|
|
Args:
|
|
file_path (str): The file path to resolve
|
|
|
|
Returns:
|
|
str: Resolved absolute path or original path if not found
|
|
"""
|
|
if not file_path or not file_path.strip():
|
|
return None
|
|
|
|
file_path = file_path.strip()
|
|
|
|
# If already absolute and exists, return as-is
|
|
if os.path.isabs(file_path) and os.path.isfile(file_path):
|
|
return file_path
|
|
|
|
# Try relative to current script directory
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
relative_path = os.path.join(script_dir, file_path)
|
|
if os.path.isfile(relative_path):
|
|
return relative_path
|
|
|
|
# Try relative to example_files directory
|
|
example_files_path = os.path.join(script_dir, "example_files", file_path)
|
|
if os.path.isfile(example_files_path):
|
|
return example_files_path
|
|
|
|
# Return original path (will cause error in load_text_line if not found)
|
|
return file_path
|
|
|
|
def _load_wildcard_line(self, file_path, line_index, seed):
|
|
"""
|
|
Load a line from a wildcard file using the TextFileLoader logic.
|
|
|
|
Args:
|
|
file_path (str): Path to the wildcard file
|
|
line_index (int): Index of line to select (-1 for random)
|
|
seed (int): Seed for random selection
|
|
|
|
Returns:
|
|
str: Selected line or error message
|
|
"""
|
|
try:
|
|
resolved_path = self._resolve_file_path(file_path)
|
|
if not resolved_path:
|
|
return f"Error: Empty file path"
|
|
|
|
# Normalize path
|
|
resolved_path = os.path.normpath(resolved_path)
|
|
|
|
# Check if file exists
|
|
if not os.path.isfile(resolved_path):
|
|
return f"Error: File not found at {resolved_path}"
|
|
|
|
# Get file modification time
|
|
file_mod_time = os.path.getmtime(resolved_path)
|
|
|
|
# Use cache if available and file hasn't been modified
|
|
cache_key = resolved_path
|
|
if cache_key in text_file_cache and text_file_cache[cache_key]["mod_time"] == file_mod_time:
|
|
lines = text_file_cache[cache_key]["lines"]
|
|
else:
|
|
# Read file and store in cache
|
|
with open(resolved_path, 'r', encoding='utf-8') as f:
|
|
lines = [line.rstrip() for line in f if line.strip()]
|
|
text_file_cache[cache_key] = {
|
|
"lines": lines,
|
|
"mod_time": file_mod_time
|
|
}
|
|
|
|
# Check if file is empty
|
|
if not lines:
|
|
return f"Error: File {os.path.basename(resolved_path)} is empty"
|
|
|
|
# Select line
|
|
if line_index == -1:
|
|
# Set up random seed for consistent but different choices
|
|
current_time = time.time()
|
|
if seed == -1:
|
|
# Time-based seed ensures different choice on each run
|
|
random_seed = int(current_time * 1000) % (2**32)
|
|
else:
|
|
# User-provided seed for reproducible randomness
|
|
random_seed = seed
|
|
|
|
# Set the seed for this selection
|
|
random.seed(random_seed)
|
|
|
|
# Random selection
|
|
selected_line = random.choice(lines)
|
|
|
|
# Reset random seed to avoid affecting other randomness
|
|
random.seed(None)
|
|
else:
|
|
# Check if index is valid
|
|
if line_index < 0 or line_index >= len(lines):
|
|
return f"Error: Line index {line_index} out of range (0-{len(lines)-1}) for {os.path.basename(resolved_path)}"
|
|
selected_line = lines[line_index]
|
|
|
|
return selected_line
|
|
|
|
except Exception as e:
|
|
return f"Error loading {os.path.basename(file_path) if file_path else 'file'}: {str(e)}"
|
|
|
|
def load_multiple_wildcards(self, mode, template_text, separator, seed, **kwargs):
|
|
"""
|
|
Load multiple wildcard files and combine their content.
|
|
|
|
Args:
|
|
mode (str): "slots" or "template" mode
|
|
template_text (str): Template text with {filename.txt} placeholders
|
|
separator (str): Separator between wildcards (slots mode only)
|
|
seed (int): Seed for random selection
|
|
**kwargs: Wildcard paths, enabled flags, and line indices
|
|
|
|
Returns:
|
|
tuple: Contains the combined text and preview text
|
|
"""
|
|
try:
|
|
# Use current time as unique identifier for this execution
|
|
node_instance_id = id(self)
|
|
current_time = time.time()
|
|
last_execution_time[node_instance_id] = current_time
|
|
|
|
if mode == "template":
|
|
return self._process_template_mode(template_text, seed)
|
|
else:
|
|
return self._process_slots_mode(separator, seed, **kwargs)
|
|
|
|
except Exception as e:
|
|
error_msg = f"Multi-Wildcard Error: {str(e)}"
|
|
return (error_msg, error_msg)
|
|
|
|
def _process_template_mode(self, template_text, seed):
|
|
"""
|
|
Process template mode by finding {filename.txt} placeholders and replacing them.
|
|
|
|
Args:
|
|
template_text (str): Template with placeholders
|
|
seed (int): Seed for random selection
|
|
|
|
Returns:
|
|
tuple: Processed template text and preview text
|
|
"""
|
|
import re
|
|
|
|
# Find all {filename.txt} patterns
|
|
placeholder_pattern = r'\{([^}]+)\}'
|
|
placeholders = re.findall(placeholder_pattern, template_text)
|
|
|
|
if not placeholders:
|
|
return (template_text, template_text)
|
|
|
|
result_text = template_text
|
|
|
|
# Process each placeholder
|
|
for i, filename in enumerate(placeholders):
|
|
# Calculate seed for this wildcard (offset by position)
|
|
if seed == -1:
|
|
wildcard_seed = -1
|
|
else:
|
|
wildcard_seed = seed + i
|
|
|
|
# Load wildcard line (always random selection in template mode)
|
|
selected_line = self._load_wildcard_line(filename, -1, wildcard_seed)
|
|
|
|
# Replace the first occurrence of this placeholder
|
|
placeholder = '{' + filename + '}'
|
|
result_text = result_text.replace(placeholder, selected_line, 1)
|
|
|
|
return (result_text, result_text)
|
|
|
|
def _process_slots_mode(self, separator, seed, **kwargs):
|
|
"""
|
|
Process slots mode by loading enabled wildcards and joining with separator.
|
|
|
|
Args:
|
|
separator (str): Separator between wildcards
|
|
seed (int): Seed for random selection
|
|
**kwargs: Wildcard configuration
|
|
|
|
Returns:
|
|
tuple: Combined wildcard text and preview text
|
|
"""
|
|
selected_lines = []
|
|
|
|
# Process each wildcard slot
|
|
for i in range(1, 6): # 5 slots
|
|
path_key = f"wildcard_{i}_path"
|
|
enabled_key = f"wildcard_{i}_enabled"
|
|
line_index_key = f"wildcard_{i}_line_index"
|
|
|
|
# Get values with defaults
|
|
file_path = kwargs.get(path_key, "")
|
|
enabled = kwargs.get(enabled_key, True)
|
|
line_index = kwargs.get(line_index_key, -1)
|
|
|
|
# Skip if not enabled or no path
|
|
if not enabled or not file_path or not file_path.strip():
|
|
continue
|
|
|
|
# Calculate seed for this wildcard (offset by position)
|
|
if seed == -1:
|
|
wildcard_seed = -1
|
|
else:
|
|
wildcard_seed = seed + i
|
|
|
|
# Load wildcard line
|
|
selected_line = self._load_wildcard_line(file_path, line_index, wildcard_seed)
|
|
|
|
# Only add if not an error message
|
|
if not selected_line.startswith("Error:"):
|
|
selected_lines.append(selected_line)
|
|
else:
|
|
# Include error in output for debugging
|
|
selected_lines.append(selected_line)
|
|
|
|
# Combine with separator
|
|
if not selected_lines:
|
|
no_wildcards_msg = "No wildcards loaded"
|
|
return (no_wildcards_msg, no_wildcards_msg)
|
|
|
|
combined_text = separator.join(selected_lines)
|
|
return (combined_text, combined_text)
|
|
|
|
|
|
# Node class mappings for ComfyUI registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"MultilineTextInput": MultilineTextInput,
|
|
"TextFileLoader": TextFileLoader,
|
|
"MultiWildcardLoader": MultiWildcardLoader
|
|
}
|
|
|
|
# Display names for better UI presentation
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"MultilineTextInput": "Multiline Text Input",
|
|
"TextFileLoader": "Wildcards Text File Loader",
|
|
"MultiWildcardLoader": "Multi-Wildcard Loader"
|
|
} |