Add files via upload

This commit is contained in:
Legende
2026-05-18 18:34:38 +02:00
committed by GitHub
parent 036bb90732
commit dffd9f222a
8 changed files with 545 additions and 31 deletions
+30 -4
View File
@@ -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).
+5 -2
View File
@@ -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")
+1 -1
View File
@@ -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": {
+32 -12
View File
@@ -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")
+20 -8
View File
@@ -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)
+280 -1
View File
@@ -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:
+56 -3
View File
@@ -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):
+121
View File
@@ -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,)