From d62afd86ac748d6842de41c9f7f17062a4710a0c Mon Sep 17 00:00:00 2001 From: RegulusAlpha Date: Sun, 24 Aug 2025 14:47:29 -0400 Subject: [PATCH 1/4] remove pycache and add to gitignore --- __pycache__/__init__.cpython-312.pyc | Bin 304 -> 0 bytes __pycache__/comfy_nodes.cpython-312.pyc | Bin 6457 -> 0 bytes dynprompt/__pycache__/__init__.cpython-312.pyc | Bin 245 -> 0 bytes dynprompt/__pycache__/expander.cpython-312.pyc | Bin 11911 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 __pycache__/__init__.cpython-312.pyc delete mode 100644 __pycache__/comfy_nodes.cpython-312.pyc delete mode 100644 dynprompt/__pycache__/__init__.cpython-312.pyc delete mode 100644 dynprompt/__pycache__/expander.cpython-312.pyc diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index cf2d6317ffe07a7e179612791aac7a168c4429b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 304 zcmX@j%ge<81ZM?SWfTGF#~=<2FhLogMSzUy3@HpLj5!Rsj8TlaOi@gX43$ip%r6;% zQkqP+g#G+oT;rX69D{@7eH{Y=JpJ5*Z%Kg#T|9#Wd>kX={TzK=;c|YO%(u9c^K;WG zowu5|C%nc+yFf%eTK4 D{O3^j diff --git a/__pycache__/comfy_nodes.cpython-312.pyc b/__pycache__/comfy_nodes.cpython-312.pyc deleted file mode 100644 index abcbc2de0fbf4864bd249879cdbd57fa90407018..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6457 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 diff --git a/dynprompt/__pycache__/__init__.cpython-312.pyc b/dynprompt/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 44c1d280bd20c4a2e87661e0118412062e9f8f05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 245 zcmX@j%ge<81ZM?SWrzdm#~=<2FhUuhK}x1Gq%fp2Mln<}YBJs8NUbPH%u7iv^3!Cz z#U3A@n3EG9zmnlINW-sk{oK@)%tZaf%-kG(b7RXi^RzT`-IOHbBwdqaOJiM&G{aTZlX-=wL5j)TwAlDc30ErLGjEsyoxJ5g3 K8rh3jfdT+c3`5!g diff --git a/dynprompt/__pycache__/expander.cpython-312.pyc b/dynprompt/__pycache__/expander.cpython-312.pyc deleted file mode 100644 index 2c2e0cd3db36c87e944e74f1b92abee6be1b360d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11911 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 Date: Sun, 24 Aug 2025 15:16:45 -0400 Subject: [PATCH 2/4] improve randomness --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++- comfy_nodes.py | 50 +++++++++++++++++++++------------------- dynprompt/expander.py | 43 +++++++++++++++++++++++++++++------ 3 files changed, 115 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index d7365ad..b78e7e5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A minimal **dynamic prompting + mirrored wildcards** node for ComfyUI. -## Features +## Overview - `{a|b|{c|d}}` nested choice expansion (deterministic by seed). - `__name__` loads `wildcards/name.txt`. - `__name-mir__` strictly loads `wildcards/name-mir.txt`. @@ -25,6 +25,7 @@ A minimal **dynamic prompting + mirrored wildcards** node for ComfyUI. - 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](#-variety-extra-randomness-lanes)
Wire the outputs into your usual `CLIP Text Encode` node
You can preview what the output is with the "preview any" node in utils @@ -109,6 +110,28 @@ Behavior: - 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 gets **`blue 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** +```text +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__`. @@ -127,4 +150,32 @@ Negative: lowres, bad anatomy, __hats-mir__ - If `negative` is blank and `auto_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. +- **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. +- **Deep mirroring (strict)**: `__foo-mir__` contributes mirrored exclusions to Negative even when **nested** inside other wildcards, and **only** reads `foo-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. +- **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** +```text +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) diff --git a/comfy_nodes.py b/comfy_nodes.py index 93d356c..b938b86 100644 --- a/comfy_nodes.py +++ b/comfy_nodes.py @@ -4,7 +4,6 @@ from typing import List from .dynprompt.expander import ( PromptExpander, WILDCARD_TOKEN_RE, - _stable_pick_index, # reuse same salt/index math as the expander ) # -------------------- Small helpers -------------------- @@ -28,26 +27,26 @@ def _merge_csv(*parts: str) -> str: out.append(chunk) return ", ".join(out) -def _collect_mirrors_deep(text: str, expander: PromptExpander) -> str: +def _collect_mirrors_deep(text: str, base_expander: PromptExpander) -> str: """ - Deep mirroring pass: - - Simulate the expander's left-to-right wildcard pass on the POSITIVE prompt. - - For tokens ending with '-mir', compute the chosen index and collect - "all except chosen" into a bag. (STRICT lookup: '-mir.txt' only.) - - For non-mir tokens, just replace with the chosen option (no collection). - Notes: - * We collapse braces BEFORE scanning for wildcards to match the expander's order. - * Empty branches in -mir files are supported: - - If chosen == '' (empty), we add all NON-empty others to Negative. - - If chosen is non-empty, we add all other NON-empty options. - * Newly introduced nested wildcards (from chosen options) are handled - by continuing the while-loop until no __token__ remains. + Deep mirroring pass with counter-based RNG: + - Use a fresh expander (same seed/wildcard_dir/variety) so counters start at 0. + - Collapse braces first (consumes choice counter). + - Expand wildcards left-to-right; for any token ending with '-mir', add + "all except chosen" (ignore empties) to the bag. + - Replace tokens with their chosen values to permit nested discovery. """ - # Step 1: match expander's first phase (choices first) - s = expander.collapse_choices(text or "") + mirror = PromptExpander( + seed=base_expander.seed, + wildcard_dir=base_expander.wildcard_dir, + variety=getattr(base_expander, "variety", 0), + ) + + # choices first (consumes mirror's choice counter) + s = mirror.collapse_choices(text or "") bag: List[str] = [] - # Step 2: left-to-right wildcard pass (mirrors expander._expand_wildcards_recursively) + # wildcard pass (consumes mirror's wild counter in the same sequence as Positive) while True: m = WILDCARD_TOKEN_RE.search(s) if not m: @@ -56,16 +55,15 @@ def _collect_mirrors_deep(text: str, expander: PromptExpander) -> str: token = m.group(1) start, end = m.span() - opts = expander.read_options_for_token(token) # STRICT; may be empty + opts = mirror.read_options_for_token(token) if not opts: - # Drop unknown token to mirror main expander behavior + # drop unknown to mirror main expander behavior s = s[:start] + s[end:] continue - idx = _stable_pick_index(expander.seed, f"wild:{token}:{s}:{start}-{end}", len(opts)) + idx = mirror._pick_wild_index(token, len(opts)) chosen = opts[idx] if 0 <= idx < len(opts) else "" - # If this is a strict '-mir' token, accumulate "all except chosen" (ignore empties) if token.endswith("-mir"): others = [o for i, o in enumerate(opts) if i != idx and o.strip()] if others: @@ -87,6 +85,10 @@ class DynPromptExpand: appends deep-mirrored exclusions from ANY __*-mir__ encountered in the Positive (including those nested inside wildcard files). Strict '-mir': only '-mir.txt' is consulted; no fallback to '.txt'. + + Counter-based RNG means fixed tokens don't influence randomness; only + decision points (braces/wildcards) advance the counters. Optional 'variety' + input gives multiple reproducible randomness lanes per seed. """ @classmethod @@ -96,6 +98,8 @@ class DynPromptExpand: "text": ("STRING", {"multiline": True, "default": ""}), "negative": ("STRING", {"multiline": True, "default": ""}), "seed": ("INT", {"default": 0, "min": 0, "max": 2**31 - 1}), + "variety": ("INT", {"default": 0, "min": 0, "max": 100, + "tooltip": "Extra randomness lane. 0 = off; same seed+variety reproduce."}), "wildcard_dir": ("STRING", { "default": "", "tooltip": "Folder with wildcard .txt files (supports subfolders like clothes/tops.txt)" @@ -112,7 +116,7 @@ class DynPromptExpand: FUNCTION = "expand" CATEGORY = "prompt" - def expand(self, text: str, negative: str, seed: int, wildcard_dir: str, auto_neg_from_mir: bool): + def expand(self, text: str, negative: str, seed: int, variety: int, wildcard_dir: str, auto_neg_from_mir: bool): # Resolve wildcard dir (absolute, ~ supported). Fallback to ./wildcards next to this file. wd = (wildcard_dir or "").strip() if wd: @@ -120,7 +124,7 @@ class DynPromptExpand: if not wd: wd = os.path.abspath(os.path.join(os.path.dirname(__file__), "wildcards")) - expander = PromptExpander(seed=seed, wildcard_dir=wd) + expander = PromptExpander(seed=seed, wildcard_dir=wd, variety=variety) # A) Positive prompt: full expansion (choices -> wildcards -> choices) pos_final = expander.expand_prompt(text or "", phase="pos") diff --git a/dynprompt/expander.py b/dynprompt/expander.py index 0998c08..63a111c 100644 --- a/dynprompt/expander.py +++ b/dynprompt/expander.py @@ -115,7 +115,7 @@ def _stable_pick_index(seed: int, salt: str, n: int) -> int: class PromptExpander: """ - Expands deterministically by seed: + Expands deterministically by seed and counter-based salts: 1) {a|b|{c|d}|} --> brace choices (empty branch allowed) 2) __token__ --> wildcards from /token.txt @@ -127,16 +127,45 @@ class PromptExpander: - phase="pos": pick-one for wildcards (normal) - phase="neg": join all except chosen (used for mirroring) (empty options are ignored when joining, so they don't add commas) + + Counter-based randomness: + - Fixed text has no effect on picks. + - Choices use an incrementing choice counter (choice#0, choice#1, ...). + - Wildcards use an incrementing wild counter (wild#0, wild#1, ...), salted with token. + - Optional 'variety' integer allows multiple deterministic lanes per seed. """ - def __init__(self, seed: int, wildcard_dir: Optional[str] = None): + def __init__(self, seed: int, wildcard_dir: Optional[str] = None, variety: int = 0): self.seed = int(seed) if seed is not None else 0 self.wildcard_dir = wildcard_dir or "" + self.variety = int(variety) if variety else 0 + self._salt_extra = f":v{self.variety}" if self.variety else "" + # runtime counters (reset per expand) + self._choice_counter = 0 + self._wild_counter = 0 + + # -------------------- Counter RNG helpers -------------------- + + def _reset_counters(self): + self._choice_counter = 0 + self._wild_counter = 0 + + def _pick_choice_index(self, n: int) -> int: + idx = _stable_pick_index(self.seed, f"choice#{self._choice_counter}{self._salt_extra}", n) + self._choice_counter += 1 + return idx + + def _pick_wild_index(self, token: str, n: int) -> int: + idx = _stable_pick_index(self.seed, f"wild#{self._wild_counter}:{token}{self._salt_extra}", n) + self._wild_counter += 1 + return idx # -------------------- Public helpers -------------------- def collapse_choices(self, text: str) -> str: """Deterministically collapse all {a|b|{c|d}|} blocks. No wildcard expansion.""" + # Ensure counters match the main expand order if used standalone + self._reset_counters() return self._expand_choices_recursively(text or "") def read_options_for_token(self, token: str) -> List[str]: @@ -154,7 +183,6 @@ class PromptExpander: - For non-mir tokens: split by TOP-LEVEL '|' into options (empty branches kept as ''). """ - # Resolve and read file raw (without any brace splitting) path = self._normalize_token_to_path(token) if not path: return [] @@ -182,7 +210,6 @@ class PromptExpander: return _dedup_preserve(_flatten_brace_expr(line)) else: # Non-mir: choose among top-level alts (empties allowed) - # e.g., "{red|blue| |green}" -> ["red","blue","","green"] m = re.fullmatch(r"\{(.*)\}", line) if m: return _split_top_level_alts(m.group(1)) @@ -195,7 +222,7 @@ class PromptExpander: # are collapsed later by the brace pass). return raw - # -------------------- IO & parsing ---------------------- + # -------------------- IO & path safety ---------------------- def _normalize_token_to_path(self, token: str) -> Optional[str]: if not self.wildcard_dir: @@ -221,7 +248,7 @@ class PromptExpander: if not options: # Leave unchanged on malformed {} return s - idx = _stable_pick_index(self.seed, f"choice:{s}:{start}-{end}", len(options)) + idx = self._pick_choice_index(len(options)) chosen = options[idx] # may be '' (empty branch) return s[:start] + chosen + s[end:] @@ -246,7 +273,7 @@ class PromptExpander: # Missing file => drop token silently return s[:start] + "" + s[end:] - idx = _stable_pick_index(self.seed, f"wild:{token}:{s}:{start}-{end}", len(options)) + idx = self._pick_wild_index(token, len(options)) chosen = options[idx] if 0 <= idx < len(options) else "" if phase == "pos": @@ -269,6 +296,8 @@ class PromptExpander: def expand_prompt(self, text: str, phase: str = "pos") -> str: if not text: return "" + # Reset counters so fixed text doesn't affect picks; reproducible per call + self._reset_counters() out = text # 1) Collapse braces first (enables optional segments via empty branches) out = self._expand_choices_recursively(out) From 31349b18b407113830b586c5265fe41cc21a9b58 Mon Sep 17 00:00:00 2001 From: RegulusAlpha Date: Sun, 24 Aug 2025 15:19:04 -0400 Subject: [PATCH 3/4] readme.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b78e7e5..ae36e62 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Behavior: **Example** ```text 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) @@ -151,6 +151,7 @@ Negative: lowres, bad anatomy, __hats-mir__ 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. @@ -177,5 +178,5 @@ 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) From 5b8bf138b33f937a76f3d873b3da8082e23c48b7 Mon Sep 17 00:00:00 2001 From: RegulusAlpha Date: Sun, 24 Aug 2025 17:03:28 -0400 Subject: [PATCH 4/4] readme.md --- README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/README.md b/README.md index ae36e62..3baf6db 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,82 @@ 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**. +- 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)** +1) **Braces-first pass** (repeat until none left) + Collapse the left-most non-nested `{…}` using **choice#N**. +2) **Wildcard pass** (left → right) + Replace each `__token__` using **wild#M**: + - `__name__` → read `wildcards/name.txt` (supports subfolders like `a/b.txt`). + - `__name-mir__` → **strict** read `wildcards/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). +3) **Braces-second pass** + Collapse any `{…}` that arrived from wildcard lines. +4) **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. +- 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. +- **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.