Add files via upload
This commit is contained in:
@@ -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.
|
||||
|
||||

|
||||
|
||||
### 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
@@ -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")
|
||||
@@ -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": {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,)
|
||||
Reference in New Issue
Block a user