Files
xLegende-ComfyUI-Prompt-For…/categorized_prompt_formatter.py
2026-05-18 18:34:38 +02:00

167 lines
7.5 KiB
Python

# /ComfyUI-Prompt-Formatter/categorized_prompt_formatter.py
import yaml
import re
import random
from collections import defaultdict
# Local utility imports
from .prompt_formatter_utils import (
find_yaml_file,
parse_tag,
clean_output_string,
resolve_category_tags,
resolve_inline_choices,
INCLUDE_DIRECTIVE,
TAGS_KEY
)
class CategorizedPromptFormatter:
"""
A ComfyUI node to categorize and format tags from a prompt using a YAML file.
"""
NODE_NAME = "Categorized Prompt Formatter"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"category_definition_file": ("STRING", {"default": "prompt_categories.yaml"}),
"output_template": ("STRING", {"multiline": True, "default": "<|quality|>, <|character|>, <|clothing|>, <|setting|>"}),
},
"optional": {
"input_delimiter": ("STRING", {"default": ","}),
"output_delimiter": ("STRING", {"default": ", "}),
"strip_whitespace": ("BOOLEAN", {"default": True}),
"case_sensitive_matching": ("BOOLEAN", {"default": False}),
"handle_weights": ("BOOLEAN", {"default": True}),
"match_underscores_spaces": ("BOOLEAN", {"default": True}),
"disable_duplicates": ("BOOLEAN", {"default": False}),
"unmatched_tag_handling": (["discard", "append_end", "output_separately"], {"default": "discard"}),
"seed": ("INT", {"default": -1, "min": -1, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("formatted_prompt", "rejected_prompt")
FUNCTION = "format_prompt"
CATEGORY = "text/filtering"
def format_prompt(self, prompt, category_definition_file, output_template, **kwargs):
# --- Handle Randomness for Inline Choices and Probability ---
seed = kwargs.get("seed", -1)
used_seed = random.randint(0, 0xffffffffffffffff) if seed == -1 else seed
rng = random.Random(used_seed)
# --- 1. Load & Resolve Category Definitions ---
tag_to_categories_map = defaultdict(list)
yaml_path = find_yaml_file(category_definition_file, self.NODE_NAME)
raw_yaml_data = None
if yaml_path:
try:
with open(yaml_path, 'r', encoding='utf-8') as f:
raw_yaml_data = yaml.safe_load(f)
if not isinstance(raw_yaml_data, dict):
print(f"Warning [{self.NODE_NAME}]: YAML file '{yaml_path}' is not a dictionary.")
raw_yaml_data = None
except Exception as e:
print(f"Error [{self.NODE_NAME}]: Loading YAML file {yaml_path}: {e}")
if raw_yaml_data:
resolved_tags_cache = {}
for cat_name in list(raw_yaml_data.keys()):
if str(cat_name).strip() not in[INCLUDE_DIRECTIVE, TAGS_KEY]:
resolve_category_tags(cat_name, raw_yaml_data, resolved_tags_cache, self.NODE_NAME)
case_sensitive = kwargs.get("case_sensitive_matching", False)
for cat_name, tags_set in resolved_tags_cache.items():
for tag in tags_set:
key = tag if case_sensitive else tag.lower()
if key: tag_to_categories_map[key].append(cat_name)
for key in tag_to_categories_map:
tag_to_categories_map[key] = sorted(set(tag_to_categories_map[key]))
# --- 2. Parse Input Prompt & Categorize ---
categorized_tags = defaultdict(list)
all_input_tags = set()
matched_tags = set()
handle_weights = kwargs.get("handle_weights", True)
match_spaces = kwargs.get("match_underscores_spaces", True)
case_sensitive = kwargs.get("case_sensitive_matching", False)
for raw_tag in prompt.split(kwargs.get("input_delimiter", ",")):
tag_original = raw_tag.strip() if kwargs.get("strip_whitespace", True) else raw_tag
if not tag_original: continue
all_input_tags.add(tag_original)
_, base_parsed = parse_tag(tag_original, handle_weights)
lookup_key = base_parsed if case_sensitive else base_parsed.lower()
variants = {lookup_key}
if match_spaces:
variants.add(lookup_key.replace(' ', '_'))
variants.add(lookup_key.replace('_', ' '))
for variant in sorted(variants):
if variant in tag_to_categories_map:
for category in tag_to_categories_map[variant]:
categorized_tags[category].append(tag_original)
matched_tags.add(tag_original)
break
# --- 3. Process Template ---
output_delimiter = kwargs.get("output_delimiter", ", ")
disable_dupes = kwargs.get("disable_duplicates", False)
# Resolve inline choices inside the template string first to maintain correct template markup evaluation
template_resolved = resolve_inline_choices(output_template, rng)
added_tags = set()
used_tags = set()
result_parts =[]
last_end = 0
for match in re.finditer(r"<\|([^:]+?)(?::(-?\d+))?(?::([0-9.]+))?\|>", template_resolved):
cat_name, limit_str, prob_str, start, end = match.group(1).strip(), match.group(2), match.group(3), *match.span()
result_parts.append(template_resolved[last_end:start])
probability = float(prob_str) if prob_str else 1.0
# Probability test block
if rng.random() <= probability:
limit = int(limit_str) if limit_str else None
tags_for_cat = categorized_tags.get(cat_name,[])
tags_to_process = tags_for_cat
if limit is not None:
tags_to_process = tags_for_cat[:limit] if limit > 0 else (tags_for_cat[limit:] if limit < 0 else[])
used_tags.update(tags_to_process)
tags_to_join =[t for t in tags_to_process if not (disable_dupes and t in added_tags and not added_tags.add(t))]
if tags_to_join: result_parts.append(output_delimiter.join(tags_to_join))
last_end = end
result_parts.append(template_resolved[last_end:])
formatted_prompt = "".join(result_parts)
# Second resolution step handles inline choices inside categorized tags
formatted_prompt = resolve_inline_choices(formatted_prompt, rng)
# --- 4. Handle Unmatched Tags ---
unmatched_handling = kwargs.get("unmatched_tag_handling", "discard")
rejected_prompt = ""
if unmatched_handling != "discard":
rejected_tags = sorted(all_input_tags - used_tags)
if rejected_tags:
rejected_prompt = output_delimiter.join(rejected_tags)
if unmatched_handling == "append_end":
if formatted_prompt: formatted_prompt += output_delimiter
formatted_prompt += rejected_prompt
rejected_prompt = ""
return (clean_output_string(formatted_prompt, output_delimiter), clean_output_string(rejected_prompt, output_delimiter))