fix: re-run dynamic prompts fed through linked text inputs

IS_CHANGED only receives constant inputs, so a linked text always
arrived as None and the node kept serving its cached first expansion.
Declare hidden PROMPT/UNIQUE_ID inputs and walk the prompt graph to
the upstream node: rerun only when its constants contain dynamic
syntax or cannot be statically resolved, keep caching for static
linked text.

Fixes #1120
This commit is contained in:
Will Miao
2026-09-24 09:33:27 +08:00
parent 2f9bd3ee7d
commit dae18b3d1d
4 changed files with 186 additions and 5 deletions
+15 -1
View File
@@ -7,6 +7,7 @@ from ..services.wildcard_service import (
contains_dynamic_syntax,
get_wildcard_service,
is_trigger_words_input,
linked_text_requires_rerun,
)
@@ -85,6 +86,10 @@ class PromptLM:
),
},
"optional": optional_inputs,
"hidden": {
"prompt": "PROMPT",
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = ("CONDITIONING", "STRING")
@@ -100,10 +105,16 @@ class PromptLM:
text: str,
clip: Any | None = None,
seed: int | None = None,
prompt: dict | None = None,
unique_id: str | None = None,
**kwargs: Any,
):
del clip, kwargs
if contains_dynamic_syntax(text) and seed is None:
if seed is not None:
return False
if contains_dynamic_syntax(text):
return float("NaN")
if text is None and linked_text_requires_rerun(prompt, unique_id, "text"):
return float("NaN")
return False
@@ -112,8 +123,11 @@ class PromptLM:
text: str,
clip: Any,
seed: int | None = None,
prompt: dict | None = None,
unique_id: str | None = None,
**kwargs: Any,
):
del prompt, unique_id
expanded_text = get_wildcard_service().expand_text(text, seed=seed)
trigger_words = []
+29 -4
View File
@@ -1,6 +1,10 @@
from __future__ import annotations
from ..services.wildcard_service import contains_dynamic_syntax, get_wildcard_service
from ..services.wildcard_service import (
contains_dynamic_syntax,
get_wildcard_service,
linked_text_requires_rerun,
)
class TextLM:
@@ -34,6 +38,10 @@ class TextLM:
},
),
},
"hidden": {
"prompt": "PROMPT",
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = ("STRING",)
@@ -42,10 +50,27 @@ class TextLM:
FUNCTION = "process"
@classmethod
def IS_CHANGED(cls, text: str, seed: int | None = None):
if contains_dynamic_syntax(text) and seed is None:
def IS_CHANGED(
cls,
text: str,
seed: int | None = None,
prompt: dict | None = None,
unique_id: str | None = None,
):
if seed is not None:
return False
if contains_dynamic_syntax(text):
return float("NaN")
if text is None and linked_text_requires_rerun(prompt, unique_id, "text"):
return float("NaN")
return False
def process(self, text: str, seed: int | None = None):
def process(
self,
text: str,
seed: int | None = None,
prompt: dict | None = None,
unique_id: str | None = None,
):
del prompt, unique_id
return (get_wildcard_service().expand_text(text, seed=seed),)
+45
View File
@@ -39,6 +39,51 @@ def contains_dynamic_syntax(text: str) -> bool:
)
def _is_prompt_link(value: Any) -> bool:
"""Return True for ComfyUI prompt-graph links ([node_id, output_index])."""
return (
isinstance(value, list)
and len(value) == 2
and isinstance(value[0], str)
and isinstance(value[1], (int, float))
)
def linked_text_requires_rerun(prompt: Any, node_id: Any, input_name: str) -> bool:
"""Decide if a linked text input forces re-execution for dynamic expansion.
IS_CHANGED only receives constant inputs, so a linked text arrives as None.
This walks the prompt graph to the upstream node and returns False only
when that node is fully constant and free of dynamic syntax. Dynamic
syntax — or anything that cannot be statically resolved — returns True.
"""
if not isinstance(prompt, dict) or node_id is None:
return True
node = prompt.get(str(node_id))
if not isinstance(node, dict):
return True
inputs = node.get("inputs")
if not isinstance(inputs, dict):
return True
value = inputs.get(input_name)
if not _is_prompt_link(value):
return contains_dynamic_syntax(value)
upstream = prompt.get(value[0])
if not isinstance(upstream, dict):
return True
upstream_inputs = upstream.get("inputs")
if not isinstance(upstream_inputs, dict):
return True
for upstream_value in upstream_inputs.values():
if _is_prompt_link(upstream_value):
return True
if contains_dynamic_syntax(upstream_value):
return True
return False
def get_wildcards_dir(create: bool = False) -> str:
"""Return the managed wildcard directory inside the settings folder."""