diff --git a/README.md b/README.md index e097b62..889621a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 📝🎲📊📂 ComfyUI Prompt Formatting Nodes +# 📝🎲📊📂✨ ComfyUI Prompt Formatting Nodes This repository contains custom nodes for ComfyUI designed to help structure, filter, generate, and **analyze** text prompts using categorized tag definitions stored in a YAML file. @@ -7,7 +7,8 @@ This repository contains custom nodes for ComfyUI designed to help structure, fi 1. **📝 Categorized Prompt Formatter:** Filters and restructures an *existing* input prompt based on categories and a template. Preserves tag details like weights. 2. **🎲 Categorized Random Prompt Formatter:** *Generates* a new random prompt by sampling tags from categories defined in the YAML, using a template structure and a seed for reproducibility. 3. **📊 Categorized Prompt Analyzer:** *Analyzes* an input prompt to count occurrences of specific tags or tags belonging to specified categories. -4. **📂 Wildcard Importer:** A utility node that scans a directory of `.txt` wildcard files and converts them into a single, structured YAML file, making it easy to use existing wildcard collections with this node pack. +4. **📂 Wildcard Importer:** A utility node that scans a directory of `.txt` wildcard files and converts them into a single, structured YAML file. +4. **✨ Prompt Normalizer:** A utility node that cleans up messy prompts by fixing bracket weights, removing excessive whitespace, and standardizing delimiter usage. --- @@ -16,9 +17,11 @@ This repository contains custom nodes for ComfyUI designed to help structure, fi * **YAML-based Categorization:** Define tags belonging to specific categories (e.g., `eyes`, `style`, `quality`) in an easy-to-edit YAML file. * **Hierarchical Categories (`$category`, `$include`):** Define broad categories that automatically inherit tags from more specific ones without manual repetition in the YAML, using either list-based or dictionary-based includes. * **Inline Tag Expansion:** Generate combinatorial tags within the YAML itself (e.g., `$color eyes`). -* **Template-Driven Output (Formatter/Random):** Control the structure of the output prompt using simple placeholders like `<|category_name|>`. +* **Dynamic Prompting:** Supports {weight::tag|tag}-style weighted random choices within prompts. +* **Template-Driven Output:** Control the structure of the output prompt using placeholders like `<|category_name:N|>`. * **Flexible Tag Handling:** Options for case sensitivity and matching tags regardless of underscores vs. spaces (e.g., `blue_eyes` vs `blue eyes`). -* **Wildcard Integration:** Use the **Wildcard Importer** node to automatically convert entire folders of standard `.txt` wildcards into a compatible YAML file. +* **Wildcard Integration:** Use the **Wildcard Importer** node to automatically convert entire folders of standard `.txt` wildcards into a compatible YAML files. +* **Prompt Sanitization:** Clean up bracket math, spacing, and formatting errors automatically. --- @@ -249,6 +252,29 @@ To make the node pack easy to use out-of-the-box, the **Wildcard Importer** is c --- +## 5. ✨ Prompt Normalizer + +This utility node cleans up user-input prompts, ensuring that weights and brackets are mathematically consistent and the syntax is clean. + +![Prompt Normalizer Node UI](img/normalizer_node.png) + +### Inputs + +* `prompt` (String): The raw input prompt to be cleaned. +* `weight_step` (Float): The multiplier used when converting brackets to weights (e.g., `((tag))` at 1.1 step becomes `(tag:1.21)`). Default: `1.1`. + +### Outputs + +* `normalized_prompt` (String): The cleaned, standardized prompt string. + +### Features + +* **Weight Calculation:** Converts nested brackets like `((tag))` into explicit weights like `(tag:1.21)` based on your `weight_step`. +* **Syntax Cleanup:** Standardizes spacing around commas, pipes (`|`), and brackets. +* **Bracket Balancing:** Corrects common manual entry errors in prompt bracketing. + +--- + ## Notes & Troubleshooting * **YAML Not Found:** If a node prints a warning about not finding the YAML file, double-check the filename/path and ensure it's accessible (in the node pack folder, `ComfyUI/input/`, or an absolute path). diff --git a/__init__.py b/__init__.py index 8220db7..ba9f304 100644 --- a/__init__.py +++ b/__init__.py @@ -8,6 +8,7 @@ from .categorized_prompt_formatter import CategorizedPromptFormatter from .categorized_random_prompt_formatter import CategorizedRandomPromptFormatter from .categorized_prompt_analyzer import CategorizedPromptAnalyzer from .wildcard_importer import WildcardImporter +from .prompt_normalizer import PromptNormalizer # Standardize NODE_NAME attribute in WildcardImporter for consistency if not hasattr(WildcardImporter, 'NODE_NAME'): @@ -19,6 +20,7 @@ NODE_CLASS_MAPPINGS = { CategorizedRandomPromptFormatter.NODE_NAME: CategorizedRandomPromptFormatter, CategorizedPromptAnalyzer.NODE_NAME: CategorizedPromptAnalyzer, WildcardImporter.NODE_NAME: WildcardImporter, + PromptNormalizer.NODE_NAME: PromptNormalizer, } # Mapping display names for the UI @@ -27,12 +29,13 @@ NODE_DISPLAY_NAME_MAPPINGS = { CategorizedRandomPromptFormatter.NODE_NAME: "🎲 Categorized Random Prompt Formatter", CategorizedPromptAnalyzer.NODE_NAME: "📊 Categorized Prompt Analyzer", WildcardImporter.NODE_NAME: "📂 Wildcard Importer", + PromptNormalizer.NODE_NAME: "✨ Prompt Normalizer", } -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] +__all__ =['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] # Print loading info to console -print("\n### Loading: ComfyUI-Prompt-Formatter (Version: 1.4.2) ###") +print("\n### Loading: ComfyUI-Prompt-Formatter (Version: 1.5.0) ###") for name, display_name in NODE_DISPLAY_NAME_MAPPINGS.items(): print(f" - {name} -> {display_name}") print("###-------------------------------------------###\n") \ No newline at end of file diff --git a/categorized_prompt_analyzer.py b/categorized_prompt_analyzer.py index 79bf0dc..c71654a 100644 --- a/categorized_prompt_analyzer.py +++ b/categorized_prompt_analyzer.py @@ -25,7 +25,7 @@ class CategorizedPromptAnalyzer: return { "required": { "prompt": ("STRING", {"multiline": True, "default": ""}), - "analyze_targets": ("STRING", {"multiline": False, "default": "quality, eyes, 1girl"}), + "analyze_targets": ("STRING", {"multiline": False, "default": "quality, eyes, clothing"}), "category_definition_file": ("STRING", {"default": "prompt_categories.yaml"}), }, "optional": { diff --git a/categorized_prompt_formatter.py b/categorized_prompt_formatter.py index 334b0f1..5928bb4 100644 --- a/categorized_prompt_formatter.py +++ b/categorized_prompt_formatter.py @@ -2,6 +2,7 @@ import yaml import re +import random from collections import defaultdict # Local utility imports @@ -10,6 +11,7 @@ from .prompt_formatter_utils import ( parse_tag, clean_output_string, resolve_category_tags, + resolve_inline_choices, INCLUDE_DIRECTIVE, TAGS_KEY ) @@ -37,6 +39,7 @@ class CategorizedPromptFormatter: "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}), } } @@ -46,6 +49,11 @@ class CategorizedPromptFormatter: 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) @@ -108,29 +116,41 @@ class CategorizedPromptFormatter: # --- 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+))?\|>", output_template): - cat_name, limit_str, start, end = match.group(1).strip(), match.group(2), *match.span() - result_parts.append(output_template[last_end:start]) + 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]) - limit = int(limit_str) if limit_str else None - tags_for_cat = categorized_tags.get(cat_name,[]) + probability = float(prob_str) if prob_str else 1.0 - 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[]) + # 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)) + 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(output_template[last_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") diff --git a/categorized_random_prompt_formatter.py b/categorized_random_prompt_formatter.py index 2ced917..e194c3d 100644 --- a/categorized_random_prompt_formatter.py +++ b/categorized_random_prompt_formatter.py @@ -9,6 +9,7 @@ from .prompt_formatter_utils import ( find_yaml_file, clean_output_string, resolve_category_tags, + resolve_inline_choices, INCLUDE_DIRECTIVE, TAGS_KEY ) @@ -27,7 +28,7 @@ class CategorizedRandomPromptFormatter: "category_definition_file": ("STRING", {"default": "prompt_categories.yaml"}), "output_template": ("STRING", { "multiline": True, - "default": "<|quality:1|>, <|character:1|>, <|details:3|>, <|setting:1|>" + "default": "<|quality:1|>, <|character:1|>, <|setting:1|>" }), "seed": ("INT", {"default": -1, "min": -1, "max": 0xffffffffffffffff}), }, @@ -62,20 +63,27 @@ class CategorizedRandomPromptFormatter: if raw_yaml_data: cache = {} for cat_name in list(raw_yaml_data.keys()): - if str(cat_name).strip() not in [INCLUDE_DIRECTIVE, TAGS_KEY]: + if str(cat_name).strip() not in[INCLUDE_DIRECTIVE, TAGS_KEY]: resolved_categories[str(cat_name).strip()] = resolve_category_tags(cat_name, raw_yaml_data, cache, self.NODE_NAME) else: return ("", used_seed) # --- 3. Process Template & Generate Prompt --- + # First resolve inline choices in template to avoid breaking placeholder syntax if any + template_resolved = resolve_inline_choices(output_template, rng) + result_parts =[] last_end = 0 - for match in re.finditer(r"<\|([^:]+?)(?::(\d+))?\|>", output_template): - cat_name, count_str, start, end = match.group(1).strip(), match.group(2), *match.span() - result_parts.append(output_template[last_end:start]) + + for match in re.finditer(r"<\|([^:]+?)(?::(-?\d+))?(?::([0-9.]+))?\|>", template_resolved): + cat_name, count_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 num_to_pick = int(count_str) if count_str and int(count_str) >= 0 else 1 - if num_to_pick > 0: + + # Probability test block + if num_to_pick > 0 and rng.random() <= probability: available_tags = list(resolved_categories.get(cat_name,[])) if available_tags: sample_count = min(num_to_pick, len(available_tags)) @@ -86,7 +94,11 @@ class CategorizedRandomPromptFormatter: last_end = end - result_parts.append(output_template[last_end:]) - final_prompt = clean_output_string("".join(result_parts), output_delimiter) + result_parts.append(template_resolved[last_end:]) + + # Second resolution step handles inline choices nested from inside categorized tags + joined_prompt = "".join(result_parts) + final_prompt = resolve_inline_choices(joined_prompt, rng) + final_prompt = clean_output_string(final_prompt, output_delimiter) return (final_prompt, used_seed) \ No newline at end of file diff --git a/prompt_categories.yaml b/prompt_categories.yaml index fa91a78..3f5f63f 100644 --- a/prompt_categories.yaml +++ b/prompt_categories.yaml @@ -46,7 +46,6 @@ negative: - ugly - poorly drawn hands - missing limb - - blurry - floating limbs - disconnected limbs - malformed hands @@ -155,6 +154,157 @@ style: - sculpture - claymation +# --- Popular Characters--- +character: + # Anime/Manga + - naruto uzumaki + - sasuke uchiha + - monkey d luffy + - roronoa zoro + - son goku + - vegeta + - gohan + - sailor moon + - rei ayanami + - asuka langley sohryu + - shinji ikari + - mikasa ackerman + - levi ackerman + - eren yeager + - tanjiro kamado + - nezuko kamado + - zenitsu agatsuma + - inosuke hashibira + - satoru gojo + - yuji itadori + - nobara kugisaki + - megumi fushiguro + - anya forger + - yor forger + - loid forger + - edward elric + - alphonse elric + - spike spiegel + - vash the stampede + - jotaro kujo + - dio brando + - jolyne cujoh + - killua zoldyck + - gon freecss + - hisoka morow + - kurapika + - saber (fate) + - rin tosaka + - makima + - power (chainsaw man) + - denji (chainsaw man) + - rem + - emilia + - megumin + - aqua (konosuba) + - saitama + - tatsumaki + + # Gaming + - mario + - luigi + - princess peach + - bowser + - link + - zelda + - ganondorf + - pikachu + - charizard + - mewtwo + - kirby + - samus aran + - solid snake + - cloud strife + - sephiroth + - tifa lockhart + - aerith gainsborough + - kratos + - nathan drake + - aloy + - ellie (the last of us) + - joel miller + - gordon freeman + - doom slayer + - master chief + - cortana + - 2b + - 9s + - jinx + - ahri + - lux + - yasuo + - tracer + - d.va + - mercy + - reinhardt + - genji + - lara croft + - geralt of rivia + - ciri + - yennefer of vengerberg + - triss merigold + - isabelle + - sonic the hedgehog + - shadow the hedgehog + - bayonetta + - dante (devil may cry) + - vergil (devil may cry) + + # Genshin Impact / Hoyoverse + - raiden shogun + - yae miko + - hu tao + - ganyu + - klee + - nahida + - furina + - arlecchino + - march 7th + - kafka + - bronya zaychik + - seele vollerei + - jing yuan + + # Comics / Pop Culture + - harry potter + - hermione granger + - ron weasley + - voldemort + - luke skywalker + - han solo + - princess leia + - darth vader + - yoda + - rey skywalker + - kylo ren + - captain america + - iron man + - thor + - black widow + - hulk + - loki + - spider-man + - batman + - joker + - wonder woman + - catwoman + - harley quinn + - deadpool + - wolverine + - superman + - supergirl + - storm + - magneto + - gandalf + - frodo baggins + - legolas + - aragorn + # --- Character Basics --- character_num: - 1girl @@ -546,6 +696,20 @@ skin_details: - oily skin - dry skin +face_details: + - sharp jawline + - high cheekbones + - dimples + - beauty mark + - mole + - freckles + - blushing + - nose ring + - lipstick + - eyeliner + - eyeshadow + - mascara + # --- Objects/Items --- object: - book @@ -763,6 +927,43 @@ clothing: tags: - clothing +# --- Clothing Patterns & Details --- +patterns: + - stripes + - polka dots + - plaid + - tartan + - floral print + - paisley + - checkerboard + - camo + - lace trim + - embroidery + - fur-trimmed + - hooded + +# --- Texture & Material Modifiers --- +texture: + - metallic + - glossy + - matte + - satin + - velvet + - silk + - leather + - denim + - lace + - fur + - transparent + - translucent + - crystalline + - wooden + - stone + - marble + - rusted + - corroded + - holographic + - iridescent # --- Actions --- action: @@ -892,6 +1093,25 @@ setting: - blizzard - foggy - windy + + # --- Weather & Time Effects --- +environment_mood: + - golden hour + - twilight + - blue hour + - moonlight + - starfall + - aurora borealis + - northern lights + - fireflies + - swirling mist + - heavy fog + - heat haze + - blizzard + - falling leaves + - cherry blossoms + - petals + - dust motes # --- Shot Type --- shot_type: @@ -916,6 +1136,23 @@ angle: - worm's eye view - pov (point of view) +# --- Compositional "Meta" Tags --- +composition: + - dynamic composition + - wide shot + - extreme close-up + - rule of thirds + - central composition + - low angle + - high angle + - bird's eye view + - worm's eye view + - symmetrical + - leading lines + - frame within a frame + - reflection + - silhouette + # --- Lighting --- lighting: - cinematic lighting @@ -933,6 +1170,24 @@ lighting: - neon lights - lens flare +# --- Fantasy Magic Systems & Effects --- +magic_effects: + - glowing aura + - floating particles + - energy burst + - runes + - magic circles + - spellcasting + - elemental fire + - elemental ice + - elemental lightning + - elemental wind + - elemental earth + - arcane symbols + - light rays + - dark energy + - sparks + # --- Camera & Composition --- camera: $include: @@ -956,6 +1211,30 @@ camera: - leading lines - dynamic angle - dynamic pose + +# --- Artistic Techniques & Mediums --- +technique: + - double exposure + - light painting + - bokeh + - motion blur + - shallow depth of field + - high contrast + - HDR + - color grading + - sepia + - monochromatic + - sketch, graphite + - charcoal + - ink wash + - impasto + - palette knife + - airbrush + - vector art + - pop surrealism + - synthwave + - vaporwave + - glitch art # --- Atmosphere & Mood --- atmosphere: diff --git a/prompt_formatter_utils.py b/prompt_formatter_utils.py index ecd5d17..628d8ba 100644 --- a/prompt_formatter_utils.py +++ b/prompt_formatter_utils.py @@ -63,7 +63,7 @@ def find_yaml_file(filename: str, node_name: str = "Prompt Formatter"): except Exception as e: print(f"Warning [{node_name}]: Error searching for ComfyUI input directory: {e}") - print(f"Warning [{node_name}]: YAML file '{filename}' not found as absolute, relative to node, or in standard input directories.") + print(f"Warning[{node_name}]: YAML file '{filename}' not found as absolute, relative to node, or in standard input directories.") return None # --- String & Tag Processing --- @@ -84,7 +84,7 @@ def parse_tag(tag: str, handle_weights: bool = True): if not handle_weights: return original_tag, original_tag base_tag = original_tag - patterns = [ + patterns =[ r"^\s*\(\s*(.*?)\s*(?::\s*[\d.]+\s*)?\)\s*$", # (tag:weight) or (tag) r"^\s*\[\s*(.*?)\s*\]\s*$", # [tag] r"^\s*\{\s*(.*?)\s*\}\s*$" # {tag} @@ -100,6 +100,59 @@ def parse_tag(tag: str, handle_weights: bool = True): return original_tag, base_tag.strip() +def resolve_inline_choices(text: str, rng) -> str: + """ + Resolves standard dynamic prompt inline choices like {red|blue|green}. + Supports weighted choices like {0.2::red|0.8::blue} and nested ones like {a|{b|c}}. + """ + if not text: + return text + + pattern = re.compile(r'\{([^{}]+)\}') + + def repl(match): + choices_str = match.group(1) + choices = choices_str.split('|') + + weighted_choices =[] + for c in choices: + c = c.strip() + if '::' in c: + parts = c.split('::', 1) + try: + weight = float(parts[0]) + choice = parts[1] + except ValueError: + weight = 1.0 + choice = c + else: + weight = 1.0 + choice = c + weighted_choices.append((weight, choice)) + + total_weight = sum(w for w, c in weighted_choices) + if total_weight <= 0: + picked = rng.choice([c for w, c in weighted_choices]) if weighted_choices else "" + else: + r = rng.uniform(0, total_weight) + upto = 0 + picked = weighted_choices[-1][1] if weighted_choices else "" + for w, c in weighted_choices: + if upto + w >= r: + picked = c + break + upto += w + + return picked + + prev_text = None + # We loop it to resolve nested brackets from the innermost layer recursively + while text != prev_text: + prev_text = text + text = pattern.sub(repl, text) + + return text + # --- YAML Category Resolution --- def resolve_category_tags(category_name: str, yaml_data: dict, resolved_cache: dict, node_name: str = "Prompt Formatter", recursion_guard: set = None): @@ -144,7 +197,7 @@ def resolve_category_tags(category_name: str, yaml_data: dict, resolved_cache: d elif isinstance(category_data, dict): if INCLUDE_DIRECTIVE in category_data: includes = category_data[INCLUDE_DIRECTIVE] - include_list = [str(inc).strip() for inc in includes] if isinstance(includes, list) else [str(includes).strip()] + include_list = [str(inc).strip() for inc in includes] if isinstance(includes, list) else[str(includes).strip()] for included_category in include_list: final_tags.update(resolve_category_tags(included_category, yaml_data, resolved_cache, node_name, recursion_guard.copy())) if TAGS_KEY in category_data and isinstance(category_data[TAGS_KEY], list): diff --git a/prompt_normalizer.py b/prompt_normalizer.py new file mode 100644 index 0000000..6fa33ec --- /dev/null +++ b/prompt_normalizer.py @@ -0,0 +1,121 @@ +# /ComfyUI-Prompt-Formatter/prompt_normalizer.py + +import re + +class PromptNormalizer: + """ + A ComfyUI node to normalize prompts: + - Normalizes brackets to their minimum matching pair + - Removes excessive whitespace + - Properly spaces commas, bracketing, and | + - Converts nested brackets ((a)) to a single bracket weight (a:1.21) + """ + NODE_NAME = "Prompt Normalizer" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"multiline": True, "default": ""}), + }, + "optional": { + "weight_step": ("FLOAT", {"default": 1.1, "min": 0.1, "max": 2.0, "step": 0.05}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("normalized_prompt",) + FUNCTION = "normalize_prompt" + CATEGORY = "text/utilities" + + def normalize_prompt(self, prompt, weight_step=1.1): + if not prompt: + return ("",) + + # 1. Clean up whitespace and spacing (brackets, pipes, multiple spacing) + text = re.sub(r'\s+', ' ', prompt) + text = re.sub(r'\s*\|\s*', '|', text) + text = re.sub(r'\s*,\s*', ', ', text) + text = re.sub(r'\(\s+', '(', text) + text = re.sub(r'\s+\)', ')', text) + text = re.sub(r'\[\s+', '[', text) + text = re.sub(r'\s+\]', ']', text) + + # 2. Split by commas while tracking outer brackets to bypass internal splits + tokens = [] + depth = 0 + current =[] + for char in text: + if char in '([{': + depth += 1 + current.append(char) + elif char in ')]}': + depth -= 1 + current.append(char) + elif char == ',' and depth <= 0: + tokens.append("".join(current)) + current =[] + depth = 0 # reset negative depth + else: + current.append(char) + if current: + tokens.append("".join(current)) + + # 3. Process individual tags securely ensuring mathematical extraction + processed_tokens =[] + for token in tokens: + tag = token.strip() + if not tag: + continue + + m = re.match(r'^([\[\(]+)(.*?)([\]\)]+)$', tag) + if m: + opens = m.group(1) + content = m.group(2) + closes = m.group(3) + + open_round = opens.count('(') + close_round = closes.count(')') + min_round = min(open_round, close_round) + + open_square = opens.count('[') + close_square = closes.count(']') + min_square = min(open_square, close_square) + + base_weight = 1.0 + text_content = content + weight_m = re.search(r':([0-9.]+)\s*$', content) + + if weight_m: + try: + base_weight = float(weight_m.group(1)) + text_content = content[:weight_m.start()].strip() + # Base webUI explicit weight consumes exactly one outer round bracket format syntax requirement + if min_round > 0: + min_round -= 1 + except ValueError: + pass + elif not content and (min_round > 0 or min_square > 0): + continue + + if min_round == 0 and min_square == 0 and abs(base_weight - 1.0) < 1e-5: + processed_tokens.append(content.strip()) + continue + + weight = base_weight * (weight_step ** min_round) * ((1.0 / weight_step) ** min_square) + + if abs(weight - 1.0) < 1e-5: + processed_tokens.append(text_content) + else: + weight_str = f"{weight:.2f}".rstrip('0').rstrip('.') + if weight_str.endswith('.'): + weight_str = weight_str[:-1] + processed_tokens.append(f"({text_content}:{weight_str})") + else: + # Target un-bracketed or highly unbalanced remnants + tag = re.sub(r'^([\[\(]+)', '', tag) + tag = re.sub(r'([\]\)]+)$', '', tag) + processed_tokens.append(tag) + + final_prompt = ", ".join(processed_tokens) + return (final_prompt,) \ No newline at end of file