mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-07 02:02:11 -03:00
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.
107 lines
3.4 KiB
Python
107 lines
3.4 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
_STACK_INPUT_PATTERN = re.compile(r"^lora_stack(?:_([ab])|(\d+))$")
|
|
|
|
|
|
def _is_stack_input(name: str) -> bool:
|
|
return bool(_STACK_INPUT_PATTERN.match(name))
|
|
|
|
|
|
def _stack_slot_number(name: str) -> int:
|
|
"""Numeric slot used to order stack inputs; legacy a/b map to 1/2."""
|
|
match = _STACK_INPUT_PATTERN.match(name)
|
|
if not match:
|
|
return -1
|
|
letter, digits = match.group(1), match.group(2)
|
|
if digits is not None:
|
|
return int(digits)
|
|
return 1 if letter == "a" else 2
|
|
|
|
|
|
class _LoraStackOptionalInputs(dict):
|
|
"""Optional-input mapping that also resolves dynamically added stack 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 ``lora_stack3``-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_stack_input(item)
|
|
|
|
def __getitem__(self, key: str) -> tuple[str, dict[str, Any]]:
|
|
if super().__contains__(key):
|
|
return super().__getitem__(key)
|
|
if _is_stack_input(key):
|
|
return (
|
|
"LORA_STACK",
|
|
{
|
|
"tooltip": "A LoRA stack to combine. Connect to add more inputs.",
|
|
},
|
|
)
|
|
raise KeyError(key)
|
|
|
|
|
|
class LoraStackCombinerLM:
|
|
NAME = "Lora Stack Combiner (LoraManager)"
|
|
CATEGORY = "Lora Manager/stackers"
|
|
DESCRIPTION = (
|
|
"Combines multiple LoRA stacks into a single stack. "
|
|
"Supports dynamic inputs: connect a stack to add more inputs."
|
|
)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
optional_inputs: dict[str, tuple[str, dict[str, Any]]] = {
|
|
"lora_stack1": (
|
|
"LORA_STACK",
|
|
{
|
|
"tooltip": "A LoRA stack to combine. Connect to add more inputs.",
|
|
},
|
|
),
|
|
"lora_stack2": (
|
|
"LORA_STACK",
|
|
{
|
|
"tooltip": "A LoRA stack to combine. Connect to add more inputs.",
|
|
},
|
|
),
|
|
}
|
|
|
|
return {
|
|
"required": {},
|
|
"optional": _LoraStackOptionalInputs(optional_inputs),
|
|
}
|
|
|
|
RETURN_TYPES = ("LORA_STACK",)
|
|
RETURN_NAMES = ("LORA_STACK",)
|
|
FUNCTION = "combine_stacks"
|
|
|
|
def combine_stacks(self, lora_stack1=None, lora_stack2=None, **kwargs):
|
|
stacks = {
|
|
"lora_stack1": lora_stack1,
|
|
"lora_stack2": lora_stack2,
|
|
}
|
|
for key, value in kwargs.items():
|
|
if _is_stack_input(key) and value is not None:
|
|
stacks[key] = value
|
|
|
|
combined_stack = []
|
|
for key in sorted(stacks, key=_stack_slot_number):
|
|
stack = stacks[key]
|
|
if stack:
|
|
combined_stack.extend(stack)
|
|
|
|
return (combined_stack,)
|