ComfyUI Extension: ComfyUI Dynamic Prompting Simplified
A minimal dynamic prompting + mirrored wildcards node for ComfyUI.
Custom Nodes (0)
README
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.txt
exists, 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_dir
to any folder. - Restart ComfyUI.
Node
DynPrompt Expand (mirrored wildcards)
- Inputs:
text
,negative
,seed
,wildcard_dir
,auto_neg_from_mir
- Outputs: expanded
text
, expandednegative
Node Inputs
- positive: positive prompt
- negative: negative prompt
- Seed: this is used to determine how the node will select tags
- allow neg from mir: automatically adds -mir tags to negative prompt without explicitly needing to add it to the negative prompt
- variety: int between 0-10; creates extra randomness 'lanes' Variety <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.
-
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.txt
only.
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 -> DSynPrompt 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 hat
for positive, the negative getsblue hat, green hat
automatically.
🎲 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.
variety
is an extra integer salt to create reproducible “lanes”.
Expansion Order (Positive)
- Braces-first pass (repeat until none left)
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.txt
only (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).
- If
auto_neg_from_mir
is 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 it’s
{a|b|{c|d}|}
, split by top-level|
(empties kept as""
). *-mir
: if it’s{a|b|{c|d}}
, flatten to leaf options["a","b","c","d"]
so “all-except-chosen” is well-defined.
- Non-mir: if it’s
- Multi-line files: each non-empty, non-comment line is an option (lines may contain braces/wildcards).
- 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.
- Choice/wildcard expansion is capped to prevent runaway recursion.
- If
negative
is blank andauto_neg_from_mir
is ON, the node scans the positive for__tokens__
and auto-builds a mirrored negative when possible.
Changelog
2025-08-24 — Counter-Based Randomness & “Variety” lanes
Why: fixed tokens could cause prompt to converge.
What changed
- Decision-only RNG: Random choices now depend only on decision points (each
{…}
block and each__wildcard__
/__*-mir__
call), not on surrounding fixed text.- Internally we hash
(seed, decision_counter[, token], variety)
where counters advance as we hit choices/wildcards left→right. - Adding/removing fixed words, commas, or spacing no longer shifts picks.
- Internally we hash
- Deterministic lanes via
variety
: New integer input that adds an extra salt.- Same
(seed, variety, prompt, files)
⇒ same outputs. - Sweep
variety = 0,1,2…
to get reproducible alternates without touching seed or prompt.
- Same
- Deep mirroring (strict):
__foo-mir__
contributes mirrored exclusions to Negative even when nested inside other wildcards, and only readsfoo-mir.txt
(never falls back). - Empty-branch support in
*-mir.txt
:{option|}
is honored.- If Positive picks
option
, Negative adds nothing for that token; if Positive picks the empty branch, Negative adds the non-empty options.
- If Positive picks
- Single-line
*-mir.txt
with nested braces:{1|2|{3|4}}
is flattened to leaves so mirroring can compute “all-except-chosen” correctly.
Behavior notes
- Fixed text no longer influences randomness. Only the number/order of decision points matters.
- Changing decision points (e.g., adding a
{…}
or another__wildcard__
) will change the sequence of draws—as expected. - Deep mirror uses the same counters as Positive, ensuring the Negative always gets the correct complement.
Example
Prompt: __color__, {A|B|{C|D}}, __pose-mir__
Same seed, same variety → same picks (pos & neg)
Change only fixed words → picks stay identical
Bump variety (e.g., 0→1) → different, reproducible picks
License: GPLv3 (same as the original)