Skip to content

Commit 0be8a76

Browse files
authored
V3 Improvements + DynamicCombo + Autogrow exposed in public API (#11345)
* Support Combo outputs in a more sane way * Remove test validate_inputs function on test node * Make curr_prefix be a list of strings instead of string for easier parsing as keys get added to dynamic types * Start to account for id prefixes from frontend, need to fix bug with nested dynamics * Ensure inputs/outputs/hidden are lists in schema finalize function, remove no longer needed 'is not None' checks * Add raw_link and extra_dict to all relevant Inputs * Make nested DynamicCombos work properly with prefixed keys on latest frontend; breaks old Autogrow, but is pretty much ready for upcoming Autogrow keys * Replace ... usage with a MISSING sentinel for clarity in nodes_logic.py * Added CustomCombo node in backend to reflect frontend node * Prepare Autogrow's expand_schema_for_dynamic to work with upcoming frontend changes * Prepare for look up table for dynamic input stuff * More progress towards dynamic input lookup function stuff * Finished converting _expand_schema_for_dynamic to be done via lookup instead of OOP to guarantee working with process isolation, did refactoring to remove old implementation + cleaning INPUT_TYPES definition including v3 hidden definition * Change order of functions * Removed some unneeded functions after dynamic refactor * Make MatchType's output default displayname "MATCHTYPE" * Fix DynamicSlot get_all * Removed redundant code - dynamic stuff no longer happens in OOP way * Natively support AnyType (*) without __ne__ hacks * Remove stray code that made it in * Remove expand_schema_for_dynamic left over on DynamicInput class * get_dynamic() on DynamicInput/Output was not doing anything anymore, so removed it * Make validate_inputs validate combo input correctly * Temporarily comment out conversion to 'new' (9 month old) COMBO format in get_input_info * Remove refrences to resources feature scrapped from V3 * Expose DynamicCombo in public API * satisfy ruff after some code got commented out * Make missing input error prettier for dynamic types * Created a Switch2 node as a side-by-side test, will likely go with Switch2 as the initial switch node * Figured out Switch situation * Pass in v3_data in IsChangedCache.get function's fingerprint_inputs, add a from_v3_data helper method to HiddenHolder * Switch order of Switch and Soft Switch nodes in file * Temp test node for MatchType * Fix missing v3_data for v1 nodes in validation * For now, remove chacking duplicate id's for dynamic types * Add Resize Image/Mask node that thanks to MatchType+DynamicCombo is 16-nodes-in-1 * Made DynamicCombo references in DCTestNode use public interface * Add an AnyTypeTestNode * Make lazy status for specific inputs on DynamicInputs work by having the values of the dictionary for check_lazy_status be a tuple, where the second element is the key of the input that can be returned * Comment out test logic nodes * Make primitive float's step make more sense * Add (and leave commented out) some potential logic nodes * Change default crop option to "center" on Resize Image/Mask node * Changed copy.copy(d) to d.copy() * Autogrow is available in stable frontend, so exposing it in public API * Use outputs id as display_name if no display_name present, remove v3 outputs id restriction that made them have to have unique IDs from the inputs * Enable Custom Combo node as stable frontend now supports it * Make id properly act like display_name on outputs * Add Batch Images/Masks/Latents node * Comment out Batch Images/Masks/Latents node for now, as Autogrow has a bug with MatchType where top connection is disconnected upon refresh * Removed code for a couple test nodes in nodes_logic.py * Add Batch Images, Batch Masks, and Batch Latents nodes with Autogrow, deprecate old Batch Images + LatentBatch nodes
1 parent 0357ed7 commit 0be8a76

File tree

11 files changed

+742
-274
lines changed

11 files changed

+742
-274
lines changed

comfy_api/latest/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from ._util import VideoCodec, VideoContainer, VideoComponents, MESH, VOXEL
1111
from . import _io_public as io
1212
from . import _ui_public as ui
13-
# from comfy_api.latest._resources import _RESOURCES as resources #noqa: F401
1413
from comfy_execution.utils import get_executing_context
1514
from comfy_execution.progress import get_progress_state, PreviewImageTuple
1615
from PIL import Image

comfy_api/latest/_io.py

Lines changed: 206 additions & 164 deletions
Large diffs are not rendered by default.

comfy_api/latest/_resources.py

Lines changed: 0 additions & 72 deletions
This file was deleted.

comfy_execution/graph.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ def get_input_info(
9797
extra_info = input_info[1]
9898
else:
9999
extra_info = {}
100+
# if input_type is a list, it is a Combo defined in outdated format; convert it.
101+
# NOTE: uncomment this when we are confident old format going away won't cause too much trouble.
102+
# if isinstance(input_type, list):
103+
# extra_info["options"] = input_type
104+
# input_type = IO.Combo.io_type
100105
return input_type, input_category, extra_info
101106

102107
class TopologicalSort:

comfy_execution/validation.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,24 @@ def validate_node_input(
2121
"""
2222
# If the types are exactly the same, we can return immediately
2323
# Use pre-union behaviour: inverse of `__ne__`
24+
# NOTE: this lets legacy '*' Any types work that override the __ne__ method of the str class.
2425
if not received_type != input_type:
2526
return True
2627

28+
# If one of the types is '*', we can return True immediately; this is the 'Any' type.
29+
if received_type == IO.AnyType.io_type or input_type == IO.AnyType.io_type:
30+
return True
31+
2732
# If the received type or input_type is a MatchType, we can return True immediately;
2833
# validation for this is handled by the frontend
2934
if received_type == IO.MatchType.io_type or input_type == IO.MatchType.io_type:
3035
return True
3136

37+
# This accounts for some custom nodes that output lists of options as the type;
38+
# if we ever want to break them on purpose, this can be removed
39+
if isinstance(received_type, list) and input_type == IO.Combo.io_type:
40+
return True
41+
3242
# Not equal, and not strings
3343
if not isinstance(received_type, str) or not isinstance(input_type, str):
3444
return False
@@ -37,6 +47,10 @@ def validate_node_input(
3747
received_types = set(t.strip() for t in received_type.split(","))
3848
input_types = set(t.strip() for t in input_type.split(","))
3949

50+
# If any of the types is '*', we can return True immediately; this is the 'Any' type.
51+
if IO.AnyType.io_type in received_types or IO.AnyType.io_type in input_types:
52+
return True
53+
4054
if strict:
4155
# In strict mode, all received types must be in the input types
4256
return received_types.issubset(input_types)

comfy_extras/nodes_latent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ def define_schema(cls):
255255
return io.Schema(
256256
node_id="LatentBatch",
257257
category="latent/batch",
258+
is_deprecated=True,
258259
inputs=[
259260
io.Latent.Input("samples1"),
260261
io.Latent.Input("samples2"),

comfy_extras/nodes_logic.py

Lines changed: 131 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from __future__ import annotations
12
from typing import TypedDict
23
from typing_extensions import override
34
from comfy_api.latest import ComfyExtension, io
45
from comfy_api.latest import _io
56

7+
# sentinel for missing inputs
8+
MISSING = object()
69

710

811
class SwitchNode(io.ComfyNode):
@@ -14,6 +17,37 @@ def define_schema(cls):
1417
display_name="Switch",
1518
category="logic",
1619
is_experimental=True,
20+
inputs=[
21+
io.Boolean.Input("switch"),
22+
io.MatchType.Input("on_false", template=template, lazy=True),
23+
io.MatchType.Input("on_true", template=template, lazy=True),
24+
],
25+
outputs=[
26+
io.MatchType.Output(template=template, display_name="output"),
27+
],
28+
)
29+
30+
@classmethod
31+
def check_lazy_status(cls, switch, on_false=None, on_true=None):
32+
if switch and on_true is None:
33+
return ["on_true"]
34+
if not switch and on_false is None:
35+
return ["on_false"]
36+
37+
@classmethod
38+
def execute(cls, switch, on_true, on_false) -> io.NodeOutput:
39+
return io.NodeOutput(on_true if switch else on_false)
40+
41+
42+
class SoftSwitchNode(io.ComfyNode):
43+
@classmethod
44+
def define_schema(cls):
45+
template = io.MatchType.Template("switch")
46+
return io.Schema(
47+
node_id="ComfySoftSwitchNode",
48+
display_name="Soft Switch",
49+
category="logic",
50+
is_experimental=True,
1751
inputs=[
1852
io.Boolean.Input("switch"),
1953
io.MatchType.Input("on_false", template=template, lazy=True, optional=True),
@@ -25,14 +59,14 @@ def define_schema(cls):
2559
)
2660

2761
@classmethod
28-
def check_lazy_status(cls, switch, on_false=..., on_true=...):
29-
# We use ... instead of None, as None is passed for connected-but-unevaluated inputs.
62+
def check_lazy_status(cls, switch, on_false=MISSING, on_true=MISSING):
63+
# We use MISSING instead of None, as None is passed for connected-but-unevaluated inputs.
3064
# This trick allows us to ignore the value of the switch and still be able to run execute().
3165

3266
# One of the inputs may be missing, in which case we need to evaluate the other input
33-
if on_false is ...:
67+
if on_false is MISSING:
3468
return ["on_true"]
35-
if on_true is ...:
69+
if on_true is MISSING:
3670
return ["on_false"]
3771
# Normal lazy switch operation
3872
if switch and on_true is None:
@@ -41,22 +75,50 @@ def check_lazy_status(cls, switch, on_false=..., on_true=...):
4175
return ["on_false"]
4276

4377
@classmethod
44-
def validate_inputs(cls, switch, on_false=..., on_true=...):
78+
def validate_inputs(cls, switch, on_false=MISSING, on_true=MISSING):
4579
# This check happens before check_lazy_status(), so we can eliminate the case where
4680
# both inputs are missing.
47-
if on_false is ... and on_true is ...:
81+
if on_false is MISSING and on_true is MISSING:
4882
return "At least one of on_false or on_true must be connected to Switch node"
4983
return True
5084

5185
@classmethod
52-
def execute(cls, switch, on_true=..., on_false=...) -> io.NodeOutput:
53-
if on_true is ...:
86+
def execute(cls, switch, on_true=MISSING, on_false=MISSING) -> io.NodeOutput:
87+
if on_true is MISSING:
5488
return io.NodeOutput(on_false)
55-
if on_false is ...:
89+
if on_false is MISSING:
5690
return io.NodeOutput(on_true)
5791
return io.NodeOutput(on_true if switch else on_false)
5892

5993

94+
class CustomComboNode(io.ComfyNode):
95+
"""
96+
Frontend node that allows user to write their own options for a combo.
97+
This is here to make sure the node has a backend-representation to avoid some annoyances.
98+
"""
99+
@classmethod
100+
def define_schema(cls):
101+
return io.Schema(
102+
node_id="CustomCombo",
103+
display_name="Custom Combo",
104+
category="utils",
105+
is_experimental=True,
106+
inputs=[io.Combo.Input("choice", options=[])],
107+
outputs=[io.String.Output()]
108+
)
109+
110+
@classmethod
111+
def validate_inputs(cls, choice: io.Combo.Type) -> bool:
112+
# NOTE: DO NOT DO THIS unless you want to skip validation entirely on the node's inputs.
113+
# I am doing that here because the widgets (besides the combo dropdown) on this node are fully frontend defined.
114+
# I need to skip checking that the chosen combo option is in the options list, since those are defined by the user.
115+
return True
116+
117+
@classmethod
118+
def execute(cls, choice: io.Combo.Type) -> io.NodeOutput:
119+
return io.NodeOutput(choice)
120+
121+
60122
class DCTestNode(io.ComfyNode):
61123
class DCValues(TypedDict):
62124
combo: str
@@ -72,14 +134,14 @@ def define_schema(cls):
72134
display_name="DCTest",
73135
category="logic",
74136
is_output_node=True,
75-
inputs=[_io.DynamicCombo.Input("combo", options=[
76-
_io.DynamicCombo.Option("option1", [io.String.Input("string")]),
77-
_io.DynamicCombo.Option("option2", [io.Int.Input("integer")]),
78-
_io.DynamicCombo.Option("option3", [io.Image.Input("image")]),
79-
_io.DynamicCombo.Option("option4", [
80-
_io.DynamicCombo.Input("subcombo", options=[
81-
_io.DynamicCombo.Option("opt1", [io.Float.Input("float_x"), io.Float.Input("float_y")]),
82-
_io.DynamicCombo.Option("opt2", [io.Mask.Input("mask1", optional=True)]),
137+
inputs=[io.DynamicCombo.Input("combo", options=[
138+
io.DynamicCombo.Option("option1", [io.String.Input("string")]),
139+
io.DynamicCombo.Option("option2", [io.Int.Input("integer")]),
140+
io.DynamicCombo.Option("option3", [io.Image.Input("image")]),
141+
io.DynamicCombo.Option("option4", [
142+
io.DynamicCombo.Input("subcombo", options=[
143+
io.DynamicCombo.Option("opt1", [io.Float.Input("float_x"), io.Float.Input("float_y")]),
144+
io.DynamicCombo.Option("opt2", [io.Mask.Input("mask1", optional=True)]),
83145
])
84146
])]
85147
)],
@@ -141,14 +203,65 @@ def execute(cls, autogrow: _io.Autogrow.Type) -> io.NodeOutput:
141203
combined = ",".join([str(x) for x in vals])
142204
return io.NodeOutput(combined)
143205

206+
class ComboOutputTestNode(io.ComfyNode):
207+
@classmethod
208+
def define_schema(cls):
209+
return io.Schema(
210+
node_id="ComboOptionTestNode",
211+
display_name="ComboOptionTest",
212+
category="logic",
213+
inputs=[io.Combo.Input("combo", options=["option1", "option2", "option3"]),
214+
io.Combo.Input("combo2", options=["option4", "option5", "option6"])],
215+
outputs=[io.Combo.Output(), io.Combo.Output()],
216+
)
217+
218+
@classmethod
219+
def execute(cls, combo: io.Combo.Type, combo2: io.Combo.Type) -> io.NodeOutput:
220+
return io.NodeOutput(combo, combo2)
221+
222+
class ConvertStringToComboNode(io.ComfyNode):
223+
@classmethod
224+
def define_schema(cls):
225+
return io.Schema(
226+
node_id="ConvertStringToComboNode",
227+
display_name="Convert String to Combo",
228+
category="logic",
229+
inputs=[io.String.Input("string")],
230+
outputs=[io.Combo.Output()],
231+
)
232+
233+
@classmethod
234+
def execute(cls, string: str) -> io.NodeOutput:
235+
return io.NodeOutput(string)
236+
237+
class InvertBooleanNode(io.ComfyNode):
238+
@classmethod
239+
def define_schema(cls):
240+
return io.Schema(
241+
node_id="InvertBooleanNode",
242+
display_name="Invert Boolean",
243+
category="logic",
244+
inputs=[io.Boolean.Input("boolean")],
245+
outputs=[io.Boolean.Output()],
246+
)
247+
248+
@classmethod
249+
def execute(cls, boolean: bool) -> io.NodeOutput:
250+
return io.NodeOutput(not boolean)
251+
144252
class LogicExtension(ComfyExtension):
145253
@override
146254
async def get_node_list(self) -> list[type[io.ComfyNode]]:
147255
return [
148-
# SwitchNode,
256+
SwitchNode,
257+
CustomComboNode,
258+
# SoftSwitchNode,
259+
# ConvertStringToComboNode,
149260
# DCTestNode,
150261
# AutogrowNamesTestNode,
151262
# AutogrowPrefixTestNode,
263+
# ComboOutputTestNode,
264+
# InvertBooleanNode,
152265
]
153266

154267
async def comfy_entrypoint() -> LogicExtension:

0 commit comments

Comments
 (0)