extended the dynamic prompt node to handle yaml and json files

This commit is contained in:
Apolyon6k
2025-09-17 20:22:53 +02:00
parent 2ed9c5108b
commit ae41f8068e
3 changed files with 171 additions and 96 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ NODE_CLASS_MAPPINGS = {
"RandomFloatToString": KN_RandomFloatToString,
"GetFileCountInOutputFolder": KN_GetFileCountInOutputFolder,
"LoadCSV": KN_CSV_Reader,
"DynamicPromptNode": DynamicPromptNode,
"DynamicPromptNode": KN_DynamicPromptNode,
}
# A dictionary that contains the friendly/humanly readable titles for the nodes
+169 -94
View File
@@ -1,33 +1,40 @@
# raw GPT output, unfixed and untested!
import random
import re
import folder_paths
from pathlib import Path
import os
from itertools import product
import json
import yaml
import folder_paths
class DynamicPromptNode:
class KN_DynamicPromptNode:
"""
Dynamic prompt generator with:
- Inline random options {option1|option2|...}
- Wildcards from ComfyUI wildcards folder: __filename__
- Nested placeholders
- Sequential combinatorial output with optional reset on template change
- Wildcards from ComfyUI wildcards folder: __category/subcategory__ or __category/*__
- Sequential combinatorial output
- Optional reset_on_change
- Cached YAML/JSON wildcard loading for performance
"""
#wildcard_folder = os.path.join(folder_paths.base_path)
wildcard_folder = Path(folder_paths.base_path,"wildcards")
combinatorial_index = 0
last_template = None
# Updated wildcard folder
wildcard_folder = Path(folder_paths.base_path, "wildcards")
# Cache for parsed YAML/JSON files: {filename: parsed_data}
_wildcard_cache = {}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"template": ("STRING", {"default": "A {happy|sad} __subjects__ with __adjective__","multiline": True, "dynamicPrompts": True}),
"template": ("STRING", {
"default": "A {happy|sad} __videogamechars/male__ with __adjective__",
"multiline": True,
"dynamicPrompts": True
}),
"seed": ("INT", {"default": -1}),
"mode": (["random", "sequential"], {"default": "random"}),
"reset_on_change": ("BOOL", {"default": True}),
"reset_on_change": ("BOOLEAN", {"default": True}),
}
}
@@ -36,98 +43,166 @@ class DynamicPromptNode:
FUNCTION = "generate"
CATEGORY = "KaleidiaNodes/PromptNodes"
# -----------------------
# Instance state
# -----------------------
def _ensure_state(self):
if not hasattr(self, "combinatorial_index"):
self.combinatorial_index = 0
self.last_template = None
def generate(self, template, seed=-1, mode="random", reset_on_change=True):
self._ensure_state()
if seed >= 0:
random.seed(seed)
else:
random.seed()
# Reset combinatorial index if template changed and option enabled
if reset_on_change and template != self.last_template:
self.combinatorial_index = 0
self.last_template = template
if mode == "random":
# Recursive random replacement
def resolve(text, depth=0):
if depth > 20:
return text
def replace_inline(match):
options = [opt.strip() for opt in match.group(1).split("|")]
choice = random.choice(options)
return resolve(choice, depth + 1)
text = re.sub(r"\{([^{}]+)\}", replace_inline, text)
def replace_wildcard(match):
filename = match.group(1).strip()
file_path = self.wildcard_folder / f"{filename}.txt"
if file_path.exists():
with open(file_path, "r", encoding="utf-8") as f:
lines = [line.strip() for line in f if line.strip()]
if lines:
choice = random.choice(lines)
return resolve(choice, depth + 1)
return f"__{filename}__"
text = re.sub(r"__([A-Za-z0-9_]+)__", replace_wildcard, text)
return text
return (resolve(template),)
return (self._resolve_random(template),)
elif mode == "sequential":
# Parse template into placeholders
placeholder_pattern = r"\{([^{}]+)\}|__([A-Za-z0-9_]+)__"
tokens = []
matches = list(re.finditer(placeholder_pattern, template))
last_index = 0
option_lists = []
for match in matches:
start, end = match.span()
tokens.append(template[last_index:start])
last_index = end
if match.group(1):
options = [opt.strip() for opt in match.group(1).split("|")]
else:
filename = match.group(2).strip()
file_path = self.wildcard_folder / f"{filename}.txt"
if file_path.exists():
with open(file_path, "r", encoding="utf-8") as f:
options = [line.strip() for line in f if line.strip()]
else:
options = [f"__{filename}__"]
tokens.append(None)
option_lists.append(options)
tokens.append(template[last_index:])
if not option_lists:
return (template,)
# Compute combination for current index
all_combinations = list(product(*option_lists))
combo_index = self.combinatorial_index % len(all_combinations)
self.combinatorial_index += 1
selected_combo = all_combinations[combo_index]
prompt = ""
combo_idx = 0
for tok in tokens:
if tok is None:
prompt += selected_combo[combo_idx]
combo_idx += 1
else:
prompt += tok
return (prompt,)
return (self._resolve_sequential(template),)
return ("<invalid mode>",)
# -----------------------
# Random / Sequential
# -----------------------
def _resolve_random(self, text, depth=0):
if depth > 20:
return text
# Register node
# NODE_CLASS_MAPPINGS.update({"DynamicPromptNode": DynamicPromptNode})
# NODE_DISPLAY_NAME_MAPPINGS.update({"DynamicPromptNode": "Dynamic Prompt Generator"})
# inline {a|b|c}
def replace_inline(match):
options = [opt.strip() for opt in match.group(1).split("|")]
choice = random.choice(options)
return self._resolve_random(choice, depth + 1)
text = re.sub(r"\{([^{}]+)\}", replace_inline, text)
# wildcards __category/subcategory__ or __category/*__
def replace_wildcard(match):
path = match.group(1).strip()
options = self._load_category_wildcard(path)
if options:
choice = random.choice(options)
return self._resolve_random(choice, depth + 1)
return f"__{path}__"
text = re.sub(r"__([A-Za-z0-9_/*]+)__", replace_wildcard, text)
return text
def _resolve_sequential(self, template):
placeholder_pattern = r"\{([^{}]+)\}|__([A-Za-z0-9_/*]+)__"
tokens = []
matches = list(re.finditer(placeholder_pattern, template))
last_index = 0
option_lists = []
for match in matches:
start, end = match.span()
tokens.append(template[last_index:start])
last_index = end
if match.group(1):
options = [opt.strip() for opt in match.group(1).split("|")]
else:
path = match.group(2).strip()
options = self._load_category_wildcard(path)
if not options:
options = [f"__{path}__"]
tokens.append(None)
option_lists.append(options)
tokens.append(template[last_index:])
if not option_lists:
return template
all_combinations = list(product(*option_lists))
combo_index = self.combinatorial_index % len(all_combinations)
self.combinatorial_index += 1
selected_combo = all_combinations[combo_index]
prompt = ""
combo_idx = 0
for tok in tokens:
if tok is None:
prompt += selected_combo[combo_idx]
combo_idx += 1
else:
prompt += tok
return prompt
# -----------------------
# Wildcard helpers
# -----------------------
def _flatten_data(self, data):
results = []
if isinstance(data, dict):
for v in data.values():
results.extend(self._flatten_data(v))
elif isinstance(data, list):
for v in data:
results.extend(self._flatten_data(v))
elif isinstance(data, str):
results.append(data.strip())
return results
def _load_category_wildcard(self, category_path):
if "/" in category_path:
root, subcat = category_path.split("/", 1)
else:
root, subcat = category_path, None
root = root.lower()
subcat = subcat.lower() if subcat else None
options = []
# TXT file
txt_file = self.wildcard_folder / f"{category_path}.txt"
if txt_file.exists():
with open(txt_file, "r", encoding="utf-8") as f:
options.extend([line.strip() for line in f if line.strip()])
# YAML / JSON files
for ext in ("yaml", "yml", "json"):
for f in self.wildcard_folder.glob(f"*.{ext}"):
if f in self._wildcard_cache:
data = self._wildcard_cache[f]
else:
with open(f, "r", encoding="utf-8") as fh:
if ext == "json":
data = json.load(fh)
else:
data = yaml.safe_load(fh)
self._wildcard_cache[f] = data
if not data:
continue
# make root keys lowercase for case-insensitive match
data_lower = {k.lower(): v for k, v in data.items() if isinstance(k, str)}
if root in data_lower:
node = data_lower[root]
# lowercase subcategory mapping if dict
if isinstance(node, dict):
node_lower = {k.lower(): v for k, v in node.items() if isinstance(k, str)}
if subcat == "*" or subcat is None:
options.extend(self._flatten_data(node_lower))
elif subcat in node_lower:
options.extend(self._flatten_data(node_lower[subcat]))
else:
# if node is list or str at root
options.extend(self._flatten_data(node))
return options
+1 -1
View File
@@ -2,7 +2,7 @@
[project]
name = "ComfyUI_KaleidiaNodes" # Unique identifier for your node. Immutable after creation.
description = "A simple set of nodes to make things easier. String Nodes and Files nodes."
version = "1.1.0" # Custom Node version. Must be semantically versioned.
version = "1.1.1" # Custom Node version. Must be semantically versioned.
license = { file = "LICENSE.txt" }
dependencies = [] # Filled in from requirements.txt