From 2640258902e386a0f419fb1a38d8d81c27a073e8 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Wed, 15 Apr 2026 20:43:21 +0800 Subject: [PATCH] fix(prompt): invalidate dynamic wildcard cache without seed (#895) --- py/nodes/prompt.py | 19 ++++++++++++++++++- py/nodes/text.py | 8 +++++++- py/services/wildcard_service.py | 9 +++++++++ tests/nodes/test_prompt_text_wildcards.py | 23 +++++++++++++++++++++++ tests/services/test_wildcard_service.py | 9 ++++++++- 5 files changed, 65 insertions(+), 3 deletions(-) diff --git a/py/nodes/prompt.py b/py/nodes/prompt.py index 1d2ae8c4..0c649099 100644 --- a/py/nodes/prompt.py +++ b/py/nodes/prompt.py @@ -3,7 +3,11 @@ from __future__ import annotations from typing import Any import inspect -from ..services.wildcard_service import get_wildcard_service, is_trigger_words_input +from ..services.wildcard_service import ( + contains_dynamic_syntax, + get_wildcard_service, + is_trigger_words_input, +) class _PromptOptionalInputs: @@ -90,6 +94,19 @@ class PromptLM: ) FUNCTION = "encode" + @classmethod + def IS_CHANGED( + cls, + text: str, + clip: Any | None = None, + seed: int | None = None, + **kwargs: Any, + ): + del clip, kwargs + if contains_dynamic_syntax(text) and seed is None: + return float("NaN") + return False + def encode( self, text: str, diff --git a/py/nodes/text.py b/py/nodes/text.py index 6b044bdd..02bc2664 100644 --- a/py/nodes/text.py +++ b/py/nodes/text.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..services.wildcard_service import get_wildcard_service +from ..services.wildcard_service import contains_dynamic_syntax, get_wildcard_service class TextLM: @@ -41,5 +41,11 @@ class TextLM: OUTPUT_TOOLTIPS = ("The text output.",) FUNCTION = "process" + @classmethod + def IS_CHANGED(cls, text: str, seed: int | None = None): + if contains_dynamic_syntax(text) and seed is None: + return float("NaN") + return False + def process(self, text: str, seed: int | None = None): return (get_wildcard_service().expand_text(text, seed=seed),) diff --git a/py/services/wildcard_service.py b/py/services/wildcard_service.py index 064f8ff9..248ec23c 100644 --- a/py/services/wildcard_service.py +++ b/py/services/wildcard_service.py @@ -31,6 +31,14 @@ def _is_numeric_string(value: str) -> bool: return bool(_NUMERIC_PATTERN.match(value)) +def contains_dynamic_syntax(text: str) -> bool: + """Return True when text contains supported wildcard or option syntax.""" + + return isinstance(text, str) and bool( + _WILDCARD_PATTERN.search(text) or _OPTION_PATTERN.search(text) + ) + + def get_wildcards_dir(create: bool = False) -> str: """Return the managed wildcard directory inside the settings folder.""" @@ -397,6 +405,7 @@ def get_wildcard_service() -> WildcardService: __all__ = [ "WildcardService", + "contains_dynamic_syntax", "get_wildcard_service", "get_wildcards_dir", "is_trigger_words_input", diff --git a/tests/nodes/test_prompt_text_wildcards.py b/tests/nodes/test_prompt_text_wildcards.py index fba607d2..0ab8e91b 100644 --- a/tests/nodes/test_prompt_text_wildcards.py +++ b/tests/nodes/test_prompt_text_wildcards.py @@ -59,3 +59,26 @@ def test_text_lm_input_types_expose_input_only_seed(): assert seed_type == "INT" assert seed_options["forceInput"] is True assert "wildcard generation" in seed_options["tooltip"] + + +def test_text_lm_is_changed_forces_rerun_without_seed_when_text_is_dynamic(): + result = TextLM.IS_CHANGED("__flower__", seed=None) + + assert result != result + + +def test_text_lm_is_changed_keeps_cache_for_seeded_or_static_text(): + assert TextLM.IS_CHANGED("__flower__", seed=7) is False + assert TextLM.IS_CHANGED("plain text", seed=None) is False + assert TextLM.IS_CHANGED("{red|blue}", seed=7) is False + + +def test_prompt_lm_is_changed_forces_rerun_without_seed_when_text_is_dynamic(): + result = PromptLM.IS_CHANGED("{red|blue}", clip="clip", seed=None) + + assert result != result + + +def test_prompt_lm_is_changed_keeps_cache_for_seeded_or_static_text(): + assert PromptLM.IS_CHANGED("__flower__", clip="clip", seed=11) is False + assert PromptLM.IS_CHANGED("plain text", clip="clip", seed=None) is False diff --git a/tests/services/test_wildcard_service.py b/tests/services/test_wildcard_service.py index c1e2443b..dc298a59 100644 --- a/tests/services/test_wildcard_service.py +++ b/tests/services/test_wildcard_service.py @@ -2,7 +2,7 @@ from __future__ import annotations import json -from py.services.wildcard_service import WildcardService +from py.services.wildcard_service import WildcardService, contains_dynamic_syntax def _make_service(monkeypatch, tmp_path): @@ -121,3 +121,10 @@ def test_expand_text_leaves_unresolved_reference_visible(monkeypatch, tmp_path): wildcards_dir.mkdir() assert service.expand_text("__missing__", seed=1) == "__missing__" + + +def test_contains_dynamic_syntax_detects_wildcards_and_options(): + assert contains_dynamic_syntax("plain text") is False + assert contains_dynamic_syntax("__flower__") is True + assert contains_dynamic_syntax("{red|blue}") is True + assert contains_dynamic_syntax("{2$$, $$red|blue|green}") is True