mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-24 04:24:09 -03:00
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:
+15
-1
@@ -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
@@ -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),)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -84,3 +84,100 @@ def test_prompt_lm_is_changed_forces_rerun_without_seed_when_text_is_dynamic():
|
||||
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
|
||||
|
||||
|
||||
def _linked_prompt(upstream_inputs):
|
||||
return {
|
||||
"1": {"class_type": "TextMultiline", "inputs": upstream_inputs},
|
||||
"2": {
|
||||
"class_type": "PromptLM",
|
||||
"inputs": {"text": ["1", 0], "clip": ["3", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_prompt_lm_is_changed_forces_rerun_for_linked_dynamic_text():
|
||||
prompt = _linked_prompt({"text": "{red|blue|green}"})
|
||||
|
||||
result = PromptLM.IS_CHANGED(None, clip="clip", seed=None, prompt=prompt, unique_id="2")
|
||||
|
||||
assert result != result
|
||||
|
||||
|
||||
def test_prompt_lm_is_changed_keeps_cache_for_linked_static_text():
|
||||
prompt = _linked_prompt({"text": "a plain static prompt"})
|
||||
|
||||
assert PromptLM.IS_CHANGED(None, clip="clip", seed=None, prompt=prompt, unique_id="2") is False
|
||||
assert PromptLM.IS_CHANGED(None, clip="clip", seed=5, prompt=prompt, unique_id="2") is False
|
||||
|
||||
|
||||
def test_prompt_lm_is_changed_forces_rerun_when_linked_text_unresolvable():
|
||||
chained = _linked_prompt({"text": ["9", 0]})
|
||||
|
||||
result = PromptLM.IS_CHANGED(None, clip="clip", seed=None, prompt=chained, unique_id="2")
|
||||
|
||||
assert result != result
|
||||
assert PromptLM.IS_CHANGED(None, clip="clip", seed=None, prompt=None, unique_id="2") != 0
|
||||
missing_upstream = _linked_prompt({"text": "static"})
|
||||
missing_upstream["2"]["inputs"]["text"] = ["99", 0]
|
||||
assert (
|
||||
PromptLM.IS_CHANGED(None, clip="clip", seed=None, prompt=missing_upstream, unique_id="2")
|
||||
!= 0
|
||||
)
|
||||
|
||||
|
||||
def test_text_lm_is_changed_forces_rerun_for_linked_dynamic_text():
|
||||
prompt = _linked_prompt({"text": "__flower__"})
|
||||
|
||||
result = TextLM.IS_CHANGED(None, seed=None, prompt=prompt, unique_id="2")
|
||||
|
||||
assert result != result
|
||||
|
||||
|
||||
def test_text_lm_is_changed_keeps_cache_for_linked_static_text():
|
||||
prompt = _linked_prompt({"text": "a plain static prompt"})
|
||||
|
||||
assert TextLM.IS_CHANGED(None, seed=None, prompt=prompt, unique_id="2") is False
|
||||
|
||||
|
||||
def test_text_lm_process_accepts_hidden_inputs(monkeypatch):
|
||||
node = TextLM()
|
||||
|
||||
class StubService:
|
||||
def expand_text(self, text, seed=None):
|
||||
return text
|
||||
|
||||
monkeypatch.setattr("py.nodes.text.get_wildcard_service", lambda: StubService())
|
||||
|
||||
assert node.process("hello", seed=None, prompt={}, unique_id="2") == ("hello",)
|
||||
|
||||
|
||||
def test_prompt_lm_encode_accepts_hidden_inputs(monkeypatch):
|
||||
node = PromptLM()
|
||||
|
||||
class StubService:
|
||||
def expand_text(self, text, seed=None):
|
||||
return text
|
||||
|
||||
class StubEncoder:
|
||||
def encode(self, clip, prompt):
|
||||
return ("conditioning",)
|
||||
|
||||
monkeypatch.setattr("py.nodes.prompt.get_wildcard_service", lambda: StubService())
|
||||
monkeypatch.setattr("nodes.CLIPTextEncode", lambda: StubEncoder(), raising=False)
|
||||
|
||||
result = node.encode("hello", "clip", seed=None, prompt={}, unique_id="2")
|
||||
|
||||
assert result == ("conditioning", "hello")
|
||||
|
||||
|
||||
def test_prompt_lm_input_types_declare_hidden_prompt_inputs():
|
||||
hidden = PromptLM.INPUT_TYPES()["hidden"]
|
||||
|
||||
assert hidden == {"prompt": "PROMPT", "unique_id": "UNIQUE_ID"}
|
||||
|
||||
|
||||
def test_text_lm_input_types_declare_hidden_prompt_inputs():
|
||||
hidden = TextLM.INPUT_TYPES()["hidden"]
|
||||
|
||||
assert hidden == {"prompt": "PROMPT", "unique_id": "UNIQUE_ID"}
|
||||
|
||||
Reference in New Issue
Block a user