Skip to content

Commit a580176

Browse files
committed
V3 Nodes: refactor ComfyNodeV3 class; use of ui.SavedResult; ported SaveAnimatedPNG and SaveAnimatedWEBP nodes
1 parent 371e204 commit a580176

File tree

8 files changed

+219
-114
lines changed

8 files changed

+219
-114
lines changed

comfy_api/v3/helpers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from typing import Optional, Callable
2+
3+
4+
def first_real_override(cls: type, name: str, *, base: type) -> Optional[Callable]:
5+
"""Return the *callable* override of `name` visible on `cls`, or None if every
6+
implementation up to (and including) `base` is the placeholder defined on `base`.
7+
"""
8+
base_func = getattr(base, name).__func__
9+
for c in cls.mro(): # NodeB, NodeA, ComfyNodeV3, object …
10+
if c is base: # reached the placeholder – we're done
11+
break
12+
if name in c.__dict__: # first class that *defines* the attr
13+
func = getattr(c, name).__func__
14+
if func is not base_func: # real override
15+
return getattr(cls, name) # bound to *cls*
16+
return None

comfy_api/v3/io.py

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
2-
from typing import Any, Literal, TYPE_CHECKING, TypeVar, Callable, Optional, cast, TypedDict
2+
from typing import Any, Literal, TypeVar, Callable, TypedDict
33
from typing_extensions import NotRequired
44
from enum import Enum
55
from abc import ABC, abstractmethod
@@ -108,7 +108,7 @@ class ComfyType(ABC):
108108
def comfytype(io_type: str, **kwargs):
109109
'''
110110
Decorator to mark nested classes as ComfyType; io_type will be bound to the class.
111-
111+
112112
A ComfyType may have the following attributes:
113113
- Type = <type hint here>
114114
- class Input(InputV3): ...
@@ -206,15 +206,15 @@ def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str
206206
self.socketless = socketless
207207
self.widgetType = widgetType
208208
self.force_input = force_input
209-
209+
210210
def as_dict_V1(self):
211211
return super().as_dict_V1() | prune_dict({
212212
"default": self.default,
213213
"socketless": self.socketless,
214214
"widgetType": self.widgetType,
215215
"forceInput": self.force_input,
216216
})
217-
217+
218218
def get_io_type_V1(self):
219219
return self.widgetType if self.widgetType is not None else super().get_io_type_V1()
220220

@@ -289,21 +289,21 @@ def __setattr__(self, key: str, value: Any):
289289
super().__setattr__(key, value)
290290
else:
291291
self.local_state[key] = value
292-
292+
293293
def __setitem__(self, key: str, value: Any):
294294
self.local_state[key] = value
295-
295+
296296
def __getitem__(self, key: str):
297297
return self.local_state[key]
298-
298+
299299
def __delitem__(self, key: str):
300300
del self.local_state[key]
301301

302302

303303
@comfytype(io_type="BOOLEAN")
304304
class Boolean(ComfyTypeIO):
305305
Type = bool
306-
306+
307307
class Input(WidgetInputV3):
308308
'''Boolean input.'''
309309
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
@@ -313,7 +313,7 @@ def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str
313313
self.label_on = label_on
314314
self.label_off = label_off
315315
self.default: bool
316-
316+
317317
def as_dict_V1(self):
318318
return super().as_dict_V1() | prune_dict({
319319
"label_on": self.label_on,
@@ -385,7 +385,7 @@ def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str
385385
self.multiline = multiline
386386
self.placeholder = placeholder
387387
self.default: str
388-
388+
389389
def as_dict_V1(self):
390390
return super().as_dict_V1() | prune_dict({
391391
"multiline": self.multiline,
@@ -500,7 +500,7 @@ class PooledDict(TypedDict):
500500
By default, the dimensions are based on total pixel amount, but the first value can be set to "percentage" to use a percentage of the image size instead.
501501
502502
(1024, 1024, 0, 0) would apply conditioning to the top-left 1024x1024 pixels.
503-
503+
504504
("percentage", 0.5, 0.5, 0, 0) would apply conditioning to the top-left 50% of the image.''' # TODO: verify its actually top-left
505505
strength: NotRequired[float]
506506
'''Strength of conditioning. Default strength is 1.0.'''
@@ -755,7 +755,7 @@ def __init__(self, id: str | InputV3, types: list[type[ComfyType] | ComfyType],
755755
self.input_override.widgetType = self.input_override.get_io_type_V1()
756756
super().__init__(id, display_name, optional, tooltip, lazy, extra_dict)
757757
self._io_types = types
758-
758+
759759
@property
760760
def io_types(self) -> list[type[InputV3]]:
761761
'''
@@ -768,14 +768,14 @@ def io_types(self) -> list[type[InputV3]]:
768768
else:
769769
io_types.append(x)
770770
return io_types
771-
771+
772772
def get_io_type_V1(self):
773773
# ensure types are unique and order is preserved
774774
str_types = [x.io_type for x in self.io_types]
775775
if self.input_override is not None:
776776
str_types.insert(0, self.input_override.get_io_type_V1())
777777
return ",".join(list(dict.fromkeys(str_types)))
778-
778+
779779
def as_dict_V1(self):
780780
if self.input_override is not None:
781781
return self.input_override.as_dict_V1() | super().as_dict_V1()
@@ -870,7 +870,7 @@ def __init__(self, unique_id: str, prompt: Any,
870870
def __getattr__(self, key: str):
871871
'''If hidden variable not found, return None.'''
872872
return None
873-
873+
874874
@classmethod
875875
def from_dict(cls, d: dict | None):
876876
if d is None:
@@ -1088,7 +1088,7 @@ class ComfyNodeV3:
10881088

10891089
RELATIVE_PYTHON_MODULE = None
10901090
SCHEMA = None
1091-
1091+
10921092
# filled in during execution
10931093
state: NodeState = None
10941094
resources: Resources = None
@@ -1097,28 +1097,24 @@ class ComfyNodeV3:
10971097
@classmethod
10981098
@abstractmethod
10991099
def DEFINE_SCHEMA(cls) -> SchemaV3:
1100-
"""
1101-
Override this function with one that returns a SchemaV3 instance.
1102-
"""
1103-
return None
1104-
DEFINE_SCHEMA = None
1100+
"""Override this function with one that returns a SchemaV3 instance."""
1101+
raise NotImplementedError
11051102

11061103
@classmethod
11071104
@abstractmethod
11081105
def execute(cls, **kwargs) -> NodeOutput:
1109-
pass
1110-
execute = None
1106+
raise NotImplementedError
11111107

11121108
@classmethod
11131109
def validate_inputs(cls, **kwargs) -> bool:
1114-
"""Optionally, define this function to validate inputs; equivalnet to V1's VALIDATE_INPUTS."""
1115-
pass
1116-
validate_inputs = None
1110+
"""Optionally, define this function to validate inputs; equivalent to V1's VALIDATE_INPUTS."""
1111+
raise NotImplementedError
11171112

11181113
@classmethod
11191114
def fingerprint_inputs(cls, **kwargs) -> Any:
11201115
"""Optionally, define this function to fingerprint inputs; equivalent to V1's IS_CHANGED."""
1121-
pass
1116+
raise NotImplementedError
1117+
11221118
fingerprint_inputs = None
11231119

11241120
@classmethod
@@ -1135,8 +1131,8 @@ def check_lazy_status(cls, **kwargs) -> list[str]:
11351131
11361132
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lazy_evaluation#defining-check-lazy-status
11371133
"""
1138-
need = [name for name in kwargs if kwargs[name] is None]
1139-
return need
1134+
return [name for name in kwargs if kwargs[name] is None]
1135+
11401136
check_lazy_status = None
11411137

11421138
@classmethod
@@ -1405,7 +1401,7 @@ def result(self):
14051401
@classmethod
14061402
def from_dict(cls, data: dict[str, Any]) -> "NodeOutput":
14071403
args = ()
1408-
ui = None
1404+
ui = None
14091405
expand = None
14101406
if "result" in data:
14111407
result = data["result"]

comfy_api/v3/resources.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __str__(self):
3232
class Resources(ABC):
3333
def __init__(self):
3434
...
35-
35+
3636
@abstractmethod
3737
def get(self, key: ResourceKey, default: Any=...) -> Any:
3838
pass

comfy_api/v3/ui.py

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
2-
from abc import ABC, abstractmethod
32

4-
from comfy_api.v3.io import Image, Mask, FolderType, _UIOutput, ComfyNodeV3
3+
from comfy_api.v3.io import Image, FolderType, _UIOutput, ComfyNodeV3
54
# used for image preview
65
from comfy.cli_args import args
76
import folder_paths
@@ -13,33 +12,33 @@
1312
import numpy as np
1413

1514

16-
class SavedResult:
15+
class SavedResult(dict):
1716
def __init__(self, filename: str, subfolder: str, type: FolderType):
18-
self.filename = filename
19-
self.subfolder = subfolder
20-
self.type = type
21-
22-
def as_dict(self):
23-
return {
24-
"filename": self.filename,
25-
"subfolder": self.subfolder,
26-
"type": self.type
27-
}
17+
super().__init__(filename=filename, subfolder=subfolder,type=type.value)
18+
19+
@property
20+
def filename(self) -> str:
21+
return self["filename"]
22+
23+
@property
24+
def subfolder(self) -> str:
25+
return self["subfolder"]
26+
27+
@property
28+
def type(self) -> FolderType:
29+
return FolderType(self["type"])
30+
2831

2932
class PreviewImage(_UIOutput):
3033
def __init__(self, image: Image.Type, animated: bool=False, cls: ComfyNodeV3=None, **kwargs):
3134
output_dir = folder_paths.get_temp_directory()
32-
type = "temp"
3335
prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
34-
compress_level = 1
35-
filename_prefix = "ComfyUI"
36+
filename_prefix = "ComfyUI" + prefix_append
3637

37-
filename_prefix += prefix_append
3838
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, output_dir, image[0].shape[1], image[0].shape[0])
3939
results = list()
4040
for (batch_number, image) in enumerate(image):
41-
i = 255. * image.cpu().numpy()
42-
img = PILImage.fromarray(np.clip(i, 0, 255).astype(np.uint8))
41+
img = PILImage.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8))
4342
metadata = None
4443
if not args.disable_metadata and cls is not None:
4544
metadata = PngInfo()
@@ -51,17 +50,16 @@ def __init__(self, image: Image.Type, animated: bool=False, cls: ComfyNodeV3=Non
5150

5251
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
5352
file = f"{filename_with_batch_num}_{counter:05}_.png"
54-
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=compress_level)
55-
results.append(SavedResult(file, subfolder, type))
53+
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=1)
54+
results.append(SavedResult(file, subfolder, FolderType.temp))
5655
counter += 1
57-
56+
5857
self.values = results
5958
self.animated = animated
60-
59+
6160
def as_dict(self):
62-
values = [x.as_dict() if isinstance(x, SavedResult) else x for x in self.values]
6361
return {
64-
"images": values,
62+
"images": self.values,
6563
"animated": (self.animated,)
6664
}
6765

@@ -111,36 +109,29 @@ def __init__(self, mask: PreviewMask.Type, animated: bool=False, cls: ComfyNodeV
111109
# comfy.utils.save_torch_file(output, file, metadata=metadata)
112110

113111
# self.values = values
114-
112+
115113
# def as_dict(self):
116-
# values = [x.as_dict() if isinstance(x, SavedResult) else x for x in self.values]
117114
# return {
118-
# "latents": values,
115+
# "latents": self.values,
119116
# }
120117

121118
class PreviewAudio(_UIOutput):
122119
def __init__(self, values: list[SavedResult | dict], **kwargs):
123120
self.values = values
124-
121+
125122
def as_dict(self):
126-
values = [x.as_dict() if isinstance(x, SavedResult) else x for x in self.values]
127-
return {
128-
"audio": values,
129-
}
123+
return {"audio": self.values}
130124

131125
class PreviewUI3D(_UIOutput):
132126
def __init__(self, values: list[SavedResult | dict], **kwargs):
133127
self.values = values
134-
128+
135129
def as_dict(self):
136-
values = [x.as_dict() if isinstance(x, SavedResult) else x for x in self.values]
137-
return {
138-
"3d": values,
139-
}
130+
return {"3d": self.values}
140131

141132
class PreviewText(_UIOutput):
142133
def __init__(self, value: str, **kwargs):
143134
self.value = value
144-
135+
145136
def as_dict(self):
146137
return {"text": (self.value,)}

comfy_extras/nodes_v1_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def INPUT_TYPES(cls):
2020
"mask": (IO.MASK,),
2121
}
2222
}
23-
23+
2424
RETURN_TYPES = (IO.INT, IO.IMAGE)
2525
RETURN_NAMES = ("INT", "img🖼️")
2626
OUTPUT_TOOLTIPS = (None, "This is an image")

comfy_extras/nodes_v3_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import torch
22
import time
33
from comfy_api.v3 import io, ui, resources
4-
import logging
4+
import logging # noqa
55
import folder_paths
66
import comfy.utils
77
import comfy.sd
8-
from typing import Any
8+
99

1010
@io.comfytype(io_type="XYZ")
1111
class XYZ:
@@ -88,11 +88,11 @@ def execute(cls, image: io.Image.Type, some_int: int, combo: io.Combo.Type, comb
8888
expected_int = 123
8989
if "thing" not in cls.state:
9090
cls.state["thing"] = "hahaha"
91-
yyy = cls.state["thing"]
91+
yyy = cls.state["thing"] # noqa
9292
del cls.state["thing"]
9393
if cls.state.get_value("int2") is None:
9494
cls.state.set_value("int2", 123)
95-
zzz = cls.state.get_value("int2")
95+
zzz = cls.state.get_value("int2") # noqa
9696
cls.state.pop("int2")
9797
if cls.state.my_int is None:
9898
cls.state.my_int = expected_int
@@ -175,7 +175,7 @@ def DEFINE_SCHEMA(cls):
175175
io.Image.Output(),
176176
],
177177
)
178-
178+
179179
@classmethod
180180
def validate_inputs(cls, nmock, nmock2):
181181
return True

0 commit comments

Comments
 (0)