From 6eeb06dcaef08f55abfd7973097cb72600115cbe Mon Sep 17 00:00:00 2001 From: DutchyDutch <155671438+DutchyDutch@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:26:34 +0200 Subject: [PATCH] Update wildcard_text_node.py --- wildcard_text_node.py | 116 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 10 deletions(-) diff --git a/wildcard_text_node.py b/wildcard_text_node.py index 5785ec8..66a0c50 100644 --- a/wildcard_text_node.py +++ b/wildcard_text_node.py @@ -5,7 +5,6 @@ import re import secrets from pathlib import Path -__version__ = "1.1.0" NODE_DIR = Path(__file__).resolve().parent WILDCARD_DIR = NODE_DIR / "wildcards" @@ -23,6 +22,22 @@ SUPPORTED_FILE_EXTENSIONS = [".txt", ".json", ".yaml", ".yml"] TOKEN_RE = re.compile(r"__([^\r\n]+?)__") +# Internal-only marker characters used to track which parts of the +# text were filled in by a wildcard or dynamic prompt. These never +# reach the final output - they are always either stripped out +# (for the plain text sent to generation nodes) or converted into +# visible brackets (for the on-screen preview only). +RESOLVED_START_MARKER = "\x01" +RESOLVED_END_MARKER = "\x02" + +# Bracket characters used to visually highlight resolved text in the +# preview box. Chosen deliberately because they are simple, +# single-unit Unicode characters (not a "combined" character like +# bold letters), so they render identically and safely on every +# system, while still being bold and blocky enough to stand out. +RESOLVED_BRACKET_OPEN = "【" +RESOLVED_BRACKET_CLOSE = "】" + _CACHE_SIGNATURE = None _CACHE_WILDCARDS = None _CACHE_DROPDOWN = None @@ -131,7 +146,13 @@ def token_contains_glob(token): return "*" in token or "?" in token or "[" in token -def to_unicode_bold(text): +def to_bold_dropdown_text(text): + """ + Used only for building the insert_wildcard dropdown list (folder + header names), which is a completely separate, unrelated feature + from the preview box. Left as-is since it already works fine and + is not part of the preview text pipeline. + """ normal_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" bold_upper = "𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙" @@ -155,6 +176,56 @@ def to_unicode_bold(text): return str(text).translate(translation) +def strip_resolved_markers(text): + """ + Removes the internal marker characters completely, leaving fully + plain text. This is what gets sent to the actual generation + pipeline (e.g. CLIP Text Encode), so it must never contain + special characters. + """ + text = str(text) + text = text.replace(RESOLVED_START_MARKER, "") + text = text.replace(RESOLVED_END_MARKER, "") + + return text + + +def apply_resolved_markers(text): + """ + Converts the internal start/end markers into visible brackets + for the preview box, wrapping only the outermost resolved chunk + of text - if a resolved value happens to contain further nested + markers (e.g. a wildcard result that itself contains another + wildcard), only a single outer bracket pair is shown rather than + stacking multiple brackets inside each other. + + Always produces fully clean, printable text with no control + characters left in it, safe to display anywhere. + """ + output_characters = [] + depth = 0 + + for character in str(text): + if character == RESOLVED_START_MARKER: + depth += 1 + + if depth == 1: + output_characters.append(RESOLVED_BRACKET_OPEN) + + continue + + if character == RESOLVED_END_MARKER: + if depth == 1: + output_characters.append(RESOLVED_BRACKET_CLOSE) + + depth = max(0, depth - 1) + continue + + output_characters.append(character) + + return "".join(output_characters) + + def add_dropdown_name(display_groups, directory_name, wildcard_name): directory_name = str(directory_name or ROOT_GROUP_NAME).strip() @@ -875,7 +946,7 @@ def build_grouped_dropdown(display_groups): if not wildcard_names: continue - bold_directory = to_unicode_bold(directory) + bold_directory = to_bold_dropdown_text(directory) dropdown.append(f"{HEADER_PREFIX}{bold_directory}") for name in wildcard_names: @@ -1324,12 +1395,18 @@ def expand_dynamic_block(content, wildcards, rng): options = resolve_dynamic_source_options(source_text, wildcards, rng) selected = weighted_sample_without_replacement(options, count, rng) - return separator.join(selected) + marked_selected = [ + RESOLVED_START_MARKER + item + RESOLVED_END_MARKER for item in selected + ] + + return separator.join(marked_selected) options = split_top_level(content, "|") if len(options) > 1: - return weighted_choice(options, rng) + chosen_value = weighted_choice(options, rng) + + return RESOLVED_START_MARKER + chosen_value + RESOLVED_END_MARKER return content @@ -1363,7 +1440,9 @@ def expand_wildcard_tokens_once(text, wildcards, rng): return match.group(0) changed_anything = True - return rng.choice(choices) + chosen_value = rng.choice(choices) + + return RESOLVED_START_MARKER + chosen_value + RESOLVED_END_MARKER new_text = TOKEN_RE.sub(replace_token, current_text) @@ -1381,6 +1460,17 @@ def expand_wildcards(text, wildcards, rng, max_depth=50): 1. Expand one innermost {...} block. 2. Expand __wildcard__ tokens. 3. Repeat, because wildcard results can contain more syntax. + + Returns a tuple: + (plain_text, marked_display_text) + + plain_text -> completely normal text, safe to send to + generation nodes (e.g. CLIP Text Encode). + marked_display_text -> same text, but resolved wildcard/dynamic + prompt values are wrapped in 【 】 + brackets, for the on-screen preview only. + Contains no special/control characters, so + it is always safe to display anywhere. """ current_text = str(text) @@ -1412,7 +1502,12 @@ def expand_wildcards(text, wildcards, rng, max_depth=50): if not changed_anything: break - return unescape_dynamic_prompt_text(current_text) + marked_text = unescape_dynamic_prompt_text(current_text) + + plain_text = strip_resolved_markers(marked_text) + marked_display_text = apply_resolved_markers(marked_text) + + return plain_text, marked_display_text class LocalWildcardText: @@ -1449,6 +1544,7 @@ class LocalWildcardText: "max": MAX_SEED, "step": 1, "display": "number", + "control_after_generate": False, }, ), } @@ -1472,15 +1568,15 @@ class LocalWildcardText: rng = random.Random(used_seed) wildcards, _dropdown = get_wildcard_database() - output_text = expand_wildcards(text, wildcards, rng) + plain_text, marked_display_text = expand_wildcards(text, wildcards, rng) return { "ui": { "used_seed": [str(used_seed)], - "expanded_text": [output_text], + "expanded_text": [marked_display_text], }, "result": ( - output_text, + plain_text, used_seed, ), }