Files
ComfyUI-Lora-Manager/py/nodes/prompt.py
T
Will Miao 8cd53c20f8 refactor(nodes): resolve dynamic inputs without inspect.stack()
The Prompt and Lora Stack Combiner nodes expose unbounded dynamic input
slots (trigger_wordsN / lora_stackN). They resolved them by having
INPUT_TYPES() return a custom lookup object, but only when the caller was
ComfyUI's get_input_info() -- detected with inspect.stack(). That frame
inspection is what the registry security scan reports as
python_anti_debugging under the obfuscated-code admin tag.

Make the lookup a dict subclass instead, so INPUT_TYPES() can always
return it:

  * /object_info (server.py) json.dumps INPUT_TYPES() directly, and a
    dict subclass serializes its stored entries -- byte-identical to the
    plain dict that was returned before.
  * input_order (list(value.keys())), validate_inputs'
    set(class_inputs["optional"]) and every other iteration still see only
    the static slots.
  * get_input_info() (graph.py) keeps resolving dynamic names through the
    overridden __contains__/__getitem__, which no longer depends on who
    the caller is.

The one behaviour change is in execution.py:get_input_data -- a dynamic
input passed as a constant rather than a link now reaches the node
instead of being silently dropped. These inputs are declared forceInput,
so the frontend only offers links; where it can happen the new behaviour
is the intended one.

Verified against ComfyUI's own consumer code: json.dumps output, keys(),
set(optional) and get_input_info() lookups all match the old behaviour,
and the two nodes no longer cross-resolve each other's slots.
3657 passed, 7 skipped.
2026-10-05 15:23:16 +08:00

151 lines
5.0 KiB
Python

from __future__ import annotations
from typing import Any
from ..services.wildcard_service import (
contains_dynamic_syntax,
get_wildcard_service,
is_trigger_words_input,
linked_text_requires_rerun,
)
class _PromptOptionalInputs(dict):
"""Optional-input mapping that also resolves dynamically added trigger slots.
Inheriting ``dict`` keeps ``INPUT_TYPES()`` JSON-serializable for ComfyUI's
``/object_info`` route: it serializes the stored entries, exactly as the plain
dict did before. The overridden ``__contains__``/``__getitem__`` let the
execution side resolve ``trigger_words3``-style inputs the frontend adds on
demand. This replaces the previous ``inspect.stack()`` check for the
``get_input_info`` caller, which the registry security scan reports as
anti-debugging.
"""
def __init__(self, explicit_inputs: dict[str, tuple[str, dict[str, Any]]]) -> None:
super().__init__(explicit_inputs)
def __contains__(self, item: object) -> bool:
if not isinstance(item, str):
return False
return super().__contains__(item) or is_trigger_words_input(item)
def __getitem__(self, key: str) -> tuple[str, dict[str, Any]]:
if super().__contains__(key):
return super().__getitem__(key)
if is_trigger_words_input(key):
return (
"STRING",
{
"forceInput": True,
"tooltip": "Trigger words to prepend. Connect to add more inputs.",
},
)
raise KeyError(key)
class PromptLM:
"""Encodes text (and optional trigger words) into CLIP conditioning."""
NAME = "Prompt (LoraManager)"
CATEGORY = "Lora Manager/conditioning"
DESCRIPTION = (
"Encodes a text prompt using a CLIP model into an embedding that can be used "
"to guide the diffusion model towards generating specific images. "
"Supports dynamic trigger words inputs and runtime wildcard expansion."
)
@classmethod
def INPUT_TYPES(cls):
optional_inputs: dict[str, tuple[str, dict[str, Any]]] = {
"seed": (
"INT",
{
"forceInput": True,
"tooltip": "Optional seed for wildcard generation. Leave unconnected for non-deterministic wildcard expansion.",
},
),
"trigger_words1": (
"STRING",
{
"forceInput": True,
"tooltip": "Trigger words to prepend. Connect to add more inputs.",
},
),
}
return {
"required": {
"text": (
"AUTOCOMPLETE_TEXT_PROMPT,STRING",
{
"widgetType": "AUTOCOMPLETE_TEXT_PROMPT",
"placeholder": "Enter prompt... /character, /artist, /wildcard for quick search",
"tooltip": "The text to be encoded. Wildcard references inserted with /wildcard are expanded at runtime.",
},
),
"clip": (
"CLIP",
{"tooltip": "The CLIP model used for encoding the text."},
),
},
"optional": _PromptOptionalInputs(optional_inputs),
"hidden": {
"prompt": "PROMPT",
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = ("CONDITIONING", "STRING")
RETURN_NAMES = ("CONDITIONING", "PROMPT")
OUTPUT_TOOLTIPS = (
"A conditioning containing the embedded text used to guide the diffusion model.",
)
FUNCTION = "encode"
@classmethod
def IS_CHANGED(
cls,
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 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 encode(
self,
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 = []
for key, value in kwargs.items():
if is_trigger_words_input(key) and value:
trigger_words.append(value)
if trigger_words:
prompt = ", ".join(trigger_words + [expanded_text])
else:
prompt = expanded_text
from nodes import CLIPTextEncode # pyright: ignore[reportMissingImports, reportAttributeAccessIssue]
conditioning = CLIPTextEncode().encode(clip, prompt)[0]
return (conditioning, prompt)