ComfyUI Dynamic Prompting Simplified
A minimal dynamic prompting + mirrored wildcards node for ComfyUI.
Nodes (1)
ComfyUI Dynamic Prompting Simplified
A minimal dynamic prompting + mirrored wildcards node for ComfyUI.
Overview
{a|b|{c|d}}nested choice expansion (deterministic by seed).__name__loadswildcards/name.txt.__name-mir__strictly loadswildcards/name-mir.txt.- In negative phase, if
__name__is used andname-mir.txtexists, it auto-mirrors to all options except the chosen one. - Deterministic selection per seed.
Install
- Copy this folder to:
<ComfyUI>/custom_nodes/ComfyUI-DynPromptSimplified - (Optional) Put your wildcard files in
ComfyUI-DynPromptSimplified/wildcards/or setwildcard_dirto any folder. - Restart ComfyUI.
Node
DynPrompt Expand (deep-mirrored)
- Inputs:
text,negative,seed,variety,wildcard_dir,auto_neg_from_mir - Outputs: expanded
text, expandednegative
Node Inputs
- text: positive prompt
- negative: negative prompt
- seed: this is used to determine how the node will select tags
- variety: int between 0-100; creates extra randomness 'lanes' Variety
- wildcard_dir: folder with your wildcard .txt files; leave blank to use the
wildcards/folder next to the node - auto_neg_from_mir: automatically adds -mir tags to negative prompt without explicitly needing to add it to the negative prompt <br>
Wire the outputs into your usual CLIP Text Encode node <br>
You can preview what the output is with the "preview any" node in utils
<br>
<img src="https://raw.githubusercontent.com/RegulusAlpha/ComfyUI-DynPromptSimplified/refs/heads/main/images/1.png" alt="Preview 1" width="800"/>
<br>
✨ Features
-
Nested choices with
{option1|option2|{nested1|nested2}} -
Wildcard expansion with
__name__→ expands fromwildcards/name.txt -
Line-separated wildcard files that may themselves contain braces and wildcards
-
Mirrored wildcards with
__name-mir__:- Positive prompt gets the chosen option
- Negative prompt gets all the other options, comma-separated
- Strict resolution:
__name-mir__reads onlywildcards/name-mir.txt(no fallback toname.txt)
-
Deterministic behavior using the current generation seed
-
Works for both positive and negative prompts
-
Expanded prompts are saved into PNG metadata
📑 Wildcard Files
-
Located in the
wildcards/directory (configurable). -
Each line is one possible expansion.
-
Blank lines and lines starting with
#are ignored. -
Lines may contain further braces
{}and wildcard calls.
Example: wildcards/hats.txt
{red hat|blue hat|{green hat|yellow hat|{black hat|gold hat}}}
beret
top hat
🔄 Mirrored Wildcards
- File name:
name-mir.txt(called via__name-mir__). - Ensures complementary picks between positive/negative prompts.
Example: wildcards/hats-mir.txt
{red hat|blue hat|green hat}
{tall hat|short hat|medium hat}
- Positive prompt:
portrait, __hats-mir__ - Negative prompt:
lowres, __hats-mir__- If pos →
red hat, neg →blue hat, green hat - If pos →
short hat, neg →tall hat, medium hat
- If pos →
This ensures that the negative prompt excludes the token chosen in the positive prompt.
🧩 Nested -mir behavior (auto-discovery)
Mirrored tokens can be nested inside other wildcards. If a wildcard you use in the positive prompt expands to another token like __foo-mir__, the extension can automatically inject the mirrored complement into the negative prompt (so you don’t have to add __foo-mir__ manually).
- This requires the checkbox in the UI:
“Automatically mirror -mir wildcards without explicitly adding them to the negative prompt.” (enabled by default) - The auto-inject only happens if the negative prompt does not already contain that
__*-mir__token. - Resolution is strict:
__foo-mir__readswildcards/foo-mir.txtonly.
Example (nested):
wildcards/outfits.txt
---------------------
__hats-mir__, {casual|formal}
wildcards/hats-mir.txt
----------------------
{red hat|blue hat|green hat}
Usage:
- Positive:
portrait, __outfits__ - Negative: (leave blank or put your usual negatives) <br>
- add node from: add node -> prompt -> DynPrompt Expand (deep-mirrored) <br>
Behavior:
- The positive prompt expands
__outfits__→ which contains__hats-mir__. - With the checkbox enabled, the extension auto-injects
__hats-mir__into the negative and expands it there as the comma‑separated “other” options. - If the seed picks
red hatfor positive, the negative getsblue hat, green hatautomatically.
🎲 Variety (extra randomness lanes)
What it is: An integer knob that creates alternate, reproducible randomness lanes without changing your seed or prompt.
Deterministic: With the same (seed, variety, prompt, files), you get the same positive & negative outputs every time.
Independence from fixed text: Only decision points advance randomness—each {…} block and each __wildcard__ / __*-mir__. Fixed words/commas/spaces do not affect picks.
How it works (under the hood):
- Choices use
choice#0, choice#1, … - Wildcards use
wild#0, wild#1, …(also salted with the token name) - We hash
(seed, counter[, token], variety)to pick an index.
When to use: Keep seed fixed and sweep variety = 0,1,2… to explore different, reproducible variants from the same prompt.
Example
Prompt: __color__, {A|B|{C|D}}, __pose-mir__
(seed=123, variety=0) → picks one set of branches (seed=123, variety=1) → different set of branches (still reproducible)
⚙️ Usage
- Write prompts as usual with
{}and__wildcards__. - Wildcards are recursively expanded.
- Mirrored wildcards respect complement logic.
Prompt Example:
Positive: portrait, {cinematic|studio|outdoor}, __hats__, soft lighting
Negative: lowres, bad anatomy, __hats-mir__
Logic
Determinism & Counters
- Randomness depends only on decision points:
- Each non-nested
{…}is the next choice#N. - Each
__token__(including__*-mir__) is the next wild#M.
- Each non-nested
- For each pick we hash
(seed, counter[, token], variety)→ index. - Fixed text (words/commas/spaces) does not affect counters or picks.
varietyis an extra integer salt to create reproducible “lanes”.
Expansion Order (Positive)
- Braces-first pass (repeat until none left, up to 1024 blocks)
Collapse the left-most non-nested{…}using choice#N. - Wildcard pass (left → right)
Replace each__token__using wild#M:__name__→ readwildcards/name.txt(supports subfolders likea/b.txt).__name-mir__→ strict readwildcards/name-mir.txtonly (no fallback).- If a chosen line itself contains braces/wildcards, it’s injected and will be handled by subsequent passes.
- Missing file ⇒ token disappears (empty).
- Braces-second pass
Collapse any{…}that arrived from wildcard lines. - Cleanup
Normalize commas/spaces.
Deep Mirroring (Negative complement)
- Your user Negative is expanded normally (same algorithm as Positive: braces → wildcards → braces), except:
- An explicit
__name-mir__in the Negative becomes every option the Positive did not pick for that token (never the picked one). - If the Positive never used that token, it becomes all of its options.
- An explicit
- If
auto_neg_from_miris ON, we run a mirror pass over the Positive prompt:- Re-run the same brace + wildcard passes with the same
(seed, variety)and counters. - For every
__*-mir__encountered (even if nested inside other wildcards):- Compute the chosen option.
- Add all non-empty alternatives except the chosen one to a bag.
- Special case: if the chosen alternative is empty (from
{option|}), add all non-empty options.
- Re-run the same brace + wildcard passes with the same
- Merge:
final_negative = dedup(user_negative_expanded + mirrored_bag).
File Semantics
- Single-line files:
- Non-mir: if the whole line is one block like
{a|b|{c|d}|}, split by top-level|(empties kept as""). A line like{a|b} dress, {c|d}is one option and its braces are picked normally. *-mir: braces are flattened to leaf options ({a|b|{c|d}}→["a","b","c","d"],{red|blue} hat→["red hat","blue hat"]) so “all-except-chosen” is well-defined.
- Non-mir: if the whole line is one block like
- Multi-line files: each non-empty, non-comment line is an option (lines may contain braces/wildcards).
*-mirfiles whose lines contain braces are flattened the same way, so the Negative gets plain options instead of raw{…}.
- Path safety: allows subfolders (
a/b), dotted names (style.v1); blocks traversal (..).
Pseudocode (conceptual)
counters = {choice: 0, wild: 0}
def pick_choice(options):
i = hash(seed, f"choice#{counters.choice}", variety) % len(options)
counters.choice += 1
return options[i]
def pick_wild(token, options):
i = hash(seed, f"wild#{counters.wild}:{token}", variety) % len(options)
counters.wild += 1
return options[i]
# Positive:
text = collapse_braces(text, pick_choice) # pass 1
text = expand_wildcards(text, pick_wild) # pass 2
text = collapse_braces(text, pick_choice) # pass 3
# Negative:
neg = collapse_braces(neg, pick_choice)
neg = expand_wildcards(neg, pick_wild)
neg = collapse_braces(neg, pick_choice)
# Deep mirror additions:
mirror_bag = run_positive_again_collecting_all_except_chosen_for('*-mir')
final_negative = dedup_join(neg, mirror_bag)
Key Guarantees
- Same
(seed, variety, prompt, files)⇒ same Positive & Negative. - Adding/removing fixed text does not change picks.
- Changing decision points (adding/removing a
{…}or__wildcard__) changes the sequence of draws (by design).
Notes
- Missing wildcard files resolve to empty strings.
- Expansion is capped to prevent runaway recursion: each brace pass resolves up to 1024
{…}blocks and the wildcard pass (including the mirror pass) up to 1024__tokens__. Anything past the cap is left in the prompt as-is. - With
auto_neg_from_mirON, mirrored exclusions are appended to the negative whether or not you typed a negative.
Changelog
See CHANGELOG.md.
License: GPLv3 (same as the original)