Add nesting of inputs on DynamicCombo during execution

This commit is contained in:
Jedrzej Kosinski 2025-11-17 18:49:24 -08:00
parent 159e2d02c9
commit 75cc2194ff
3 changed files with 85 additions and 19 deletions

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import copy import copy
import inspect import inspect
import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import Counter from collections import Counter
from collections.abc import Iterable from collections.abc import Iterable
@ -881,6 +882,14 @@ class AutogrowDynamic(ComfyTypeI):
curr_count += 1 curr_count += 1
return new_inputs return new_inputs
def add_dynamic_id_mapping(d: dict[str], inputs: list[Input], curr_prefix: str, self: DynamicInput=None):
dynamic = d.setdefault("dynamic_data", {})
if self is not None:
dynamic[self.id] = f"{curr_prefix}{self.id}"
for i in inputs:
if not isinstance(i, DynamicInput):
dynamic[i.id] = f"{curr_prefix}{i.id}"
@comfytype(io_type="COMFY_DYNAMICCOMBO_V3") @comfytype(io_type="COMFY_DYNAMICCOMBO_V3")
class DynamicCombo(ComfyTypeI): class DynamicCombo(ComfyTypeI):
class Option: class Option:
@ -900,8 +909,9 @@ class DynamicCombo(ComfyTypeI):
super().__init__(id, display_name, optional, tooltip, lazy, extra_dict) super().__init__(id, display_name, optional, tooltip, lazy, extra_dict)
self.options = options self.options = options
def add_to_dict_live_inputs(self, d: dict[str], live_inputs: dict[str]): def add_to_dict_live_inputs(self, d: dict[str], live_inputs: dict[str], curr_prefix=''):
# check if dynamic input's id is in live_inputs # check if dynamic input's id is in live_inputs
curr_prefix = f"{curr_prefix}{self.id}."
if self.id in live_inputs: if self.id in live_inputs:
key = live_inputs[self.id] key = live_inputs[self.id]
selected_option = None selected_option = None
@ -910,8 +920,8 @@ class DynamicCombo(ComfyTypeI):
selected_option = option selected_option = option
break break
if selected_option is not None: if selected_option is not None:
add_to_input_dict_v1(d, selected_option.inputs, live_inputs) add_to_input_dict_v1(d, selected_option.inputs, live_inputs, curr_prefix)
add_dynamic_to_dict_v1(d, self, selected_option.inputs) add_dynamic_id_mapping(d, selected_option.inputs, curr_prefix, self)
def get_dynamic(self) -> list[Input]: def get_dynamic(self) -> list[Input]:
return [input for option in self.options for input in option.inputs] return [input for option in self.options for input in option.inputs]
@ -1259,12 +1269,12 @@ def create_input_dict_v1(inputs: list[Input], live_inputs: dict[str]=None) -> di
add_to_input_dict_v1(input, inputs, live_inputs) add_to_input_dict_v1(input, inputs, live_inputs)
return input return input
def add_to_input_dict_v1(d: dict[str], inputs: list[Input], live_inputs: dict[str]=None): def add_to_input_dict_v1(d: dict[str], inputs: list[Input], live_inputs: dict[str]=None, curr_prefix=''):
for i in inputs: for i in inputs:
if isinstance(i, DynamicInput): if isinstance(i, DynamicInput):
add_to_dict_v1(i, d) add_to_dict_v1(i, d)
if live_inputs is not None: if live_inputs is not None:
i.add_to_dict_live_inputs(d, live_inputs) i.add_to_dict_live_inputs(d, live_inputs, curr_prefix)
else: else:
add_to_dict_v1(i, d) add_to_dict_v1(i, d)
@ -1279,14 +1289,59 @@ def add_to_dict_v1(i: Input, d: dict, dynamic_dict: dict=None):
value = (i.get_io_type(), as_dict, dynamic_dict) value = (i.get_io_type(), as_dict, dynamic_dict)
d.setdefault(key, {})[i.id] = value d.setdefault(key, {})[i.id] = value
def add_dynamic_to_dict_v1(d: dict[str], parent: DynamicInput, inputs: list[Input]):
dynamic = d.setdefault("dynamic_data", {})
ids = [i.id for i in inputs]
dynamic[parent.id] = {"ids": ids}
def add_to_dict_v3(io: Input | Output, d: dict): def add_to_dict_v3(io: Input | Output, d: dict):
d[io.id] = (io.get_io_type(), io.as_dict()) d[io.id] = (io.get_io_type(), io.as_dict())
def build_nested_inputs(values: dict[str], paths: dict[str]):
# NOTE: This was initially AI generated
# Tries to account for arrays as well, will likely be changed once that's in
values = values.copy()
result = {}
index_pattern = re.compile(r"^(?P<key>[A-Za-z0-9_]+)\[(?P<index>\d+)\]$")
for key, path in paths.items():
parts = path.split(".")
current = result
for i, p in enumerate(parts):
is_last = (i == len(parts) - 1)
match = index_pattern.match(p)
if match:
list_key = match.group("key")
index = int(match.group("index"))
# Ensure list exists
if list_key not in current or not isinstance(current[list_key], list):
current[list_key] = []
lst = current[list_key]
# Expand list to the needed index
while len(lst) <= index:
lst.append(None)
# Last element → assign the value directly
if is_last:
lst[index] = values.pop(key)
break
# Non-last element → ensure dict
if lst[index] is None or not isinstance(lst[index], dict):
lst[index] = {}
current = lst[index]
continue
# Normal dict key
if is_last:
current[p] = values.pop(key)
else:
current = current.setdefault(p, {})
values.update(result)
return values
class _ComfyNodeBaseInternal(_ComfyNodeInternal): class _ComfyNodeBaseInternal(_ComfyNodeInternal):

View File

@ -1,3 +1,4 @@
from typing import TypedDict
from typing_extensions import override from typing_extensions import override
from comfy_api.latest import ComfyExtension, io from comfy_api.latest import ComfyExtension, io
@ -35,6 +36,12 @@ class SwitchNode(io.ComfyNode):
class DCTestNode(io.ComfyNode): class DCTestNode(io.ComfyNode):
class DCValues(TypedDict):
combo: str
string: str
integer: int
image: io.Image.Type
@classmethod @classmethod
def define_schema(cls): def define_schema(cls):
return io.Schema( return io.Schema(
@ -52,15 +59,16 @@ class DCTestNode(io.ComfyNode):
) )
@classmethod @classmethod
def execute(cls, combo, **kwargs) -> io.NodeOutput: def execute(cls, combo: DCValues) -> io.NodeOutput:
if combo == "option1": combo_val = combo["combo"]
return io.NodeOutput(kwargs["string"]) if combo_val == "option1":
elif combo == "option2": return io.NodeOutput(combo["string"])
return io.NodeOutput(kwargs["integer"]) elif combo_val == "option2":
elif combo == "option3": return io.NodeOutput(combo["integer"])
return io.NodeOutput(kwargs["image"]) elif combo_val == "option3":
return io.NodeOutput(combo["image"])
else: else:
raise ValueError(f"Invalid combo: {combo}") raise ValueError(f"Invalid combo: {combo_val}")
class LogicExtension(ComfyExtension): class LogicExtension(ComfyExtension):

View File

@ -34,7 +34,7 @@ from comfy_execution.validation import validate_node_input
from comfy_execution.progress import get_progress_state, reset_progress_state, add_progress_handler, WebUIProgressHandler from comfy_execution.progress import get_progress_state, reset_progress_state, add_progress_handler, WebUIProgressHandler
from comfy_execution.utils import CurrentNodeContext from comfy_execution.utils import CurrentNodeContext
from comfy_api.internal import _ComfyNodeInternal, _NodeOutputInternal, first_real_override, is_class, make_locked_method_func from comfy_api.internal import _ComfyNodeInternal, _NodeOutputInternal, first_real_override, is_class, make_locked_method_func
from comfy_api.latest import io from comfy_api.latest import io, _io
class ExecutionResult(Enum): class ExecutionResult(Enum):
@ -268,6 +268,9 @@ async def _async_map_node_over_list(prompt_id, unique_id, obj, input_data_all, f
type_obj.VALIDATE_CLASS() type_obj.VALIDATE_CLASS()
class_clone = type_obj.PREPARE_CLASS_CLONE(v3_data) class_clone = type_obj.PREPARE_CLASS_CLONE(v3_data)
f = make_locked_method_func(type_obj, func, class_clone) f = make_locked_method_func(type_obj, func, class_clone)
# in case of dynamic inputs, restructure inputs to expected nested dict
if v3_data is not None and v3_data["dynamic_data"] is not None:
inputs = _io.build_nested_inputs(inputs, v3_data["dynamic_data"])
# V1 # V1
else: else:
f = getattr(obj, func) f = getattr(obj, func)