mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-07-28 00:36:32 +00:00
V3 Nodes: refactor ComfyNodeV3 class; use of ui.SavedResult; ported SaveAnimatedPNG and SaveAnimatedWEBP nodes
This commit is contained in:
parent
371e20494d
commit
a580176735
16
comfy_api/v3/helpers.py
Normal file
16
comfy_api/v3/helpers.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
|
|
||||||
|
def first_real_override(cls: type, name: str, *, base: type) -> Optional[Callable]:
|
||||||
|
"""Return the *callable* override of `name` visible on `cls`, or None if every
|
||||||
|
implementation up to (and including) `base` is the placeholder defined on `base`.
|
||||||
|
"""
|
||||||
|
base_func = getattr(base, name).__func__
|
||||||
|
for c in cls.mro(): # NodeB, NodeA, ComfyNodeV3, object …
|
||||||
|
if c is base: # reached the placeholder – we're done
|
||||||
|
break
|
||||||
|
if name in c.__dict__: # first class that *defines* the attr
|
||||||
|
func = getattr(c, name).__func__
|
||||||
|
if func is not base_func: # real override
|
||||||
|
return getattr(cls, name) # bound to *cls*
|
||||||
|
return None
|
@ -1,5 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any, Literal, TYPE_CHECKING, TypeVar, Callable, Optional, cast, TypedDict
|
from typing import Any, Literal, TypeVar, Callable, TypedDict
|
||||||
from typing_extensions import NotRequired
|
from typing_extensions import NotRequired
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
@ -108,7 +108,7 @@ T = TypeVar("T", bound=type)
|
|||||||
def comfytype(io_type: str, **kwargs):
|
def comfytype(io_type: str, **kwargs):
|
||||||
'''
|
'''
|
||||||
Decorator to mark nested classes as ComfyType; io_type will be bound to the class.
|
Decorator to mark nested classes as ComfyType; io_type will be bound to the class.
|
||||||
|
|
||||||
A ComfyType may have the following attributes:
|
A ComfyType may have the following attributes:
|
||||||
- Type = <type hint here>
|
- Type = <type hint here>
|
||||||
- class Input(InputV3): ...
|
- class Input(InputV3): ...
|
||||||
@ -206,7 +206,7 @@ class WidgetInputV3(InputV3):
|
|||||||
self.socketless = socketless
|
self.socketless = socketless
|
||||||
self.widgetType = widgetType
|
self.widgetType = widgetType
|
||||||
self.force_input = force_input
|
self.force_input = force_input
|
||||||
|
|
||||||
def as_dict_V1(self):
|
def as_dict_V1(self):
|
||||||
return super().as_dict_V1() | prune_dict({
|
return super().as_dict_V1() | prune_dict({
|
||||||
"default": self.default,
|
"default": self.default,
|
||||||
@ -214,7 +214,7 @@ class WidgetInputV3(InputV3):
|
|||||||
"widgetType": self.widgetType,
|
"widgetType": self.widgetType,
|
||||||
"forceInput": self.force_input,
|
"forceInput": self.force_input,
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_io_type_V1(self):
|
def get_io_type_V1(self):
|
||||||
return self.widgetType if self.widgetType is not None else super().get_io_type_V1()
|
return self.widgetType if self.widgetType is not None else super().get_io_type_V1()
|
||||||
|
|
||||||
@ -289,13 +289,13 @@ class NodeStateLocal(NodeState):
|
|||||||
super().__setattr__(key, value)
|
super().__setattr__(key, value)
|
||||||
else:
|
else:
|
||||||
self.local_state[key] = value
|
self.local_state[key] = value
|
||||||
|
|
||||||
def __setitem__(self, key: str, value: Any):
|
def __setitem__(self, key: str, value: Any):
|
||||||
self.local_state[key] = value
|
self.local_state[key] = value
|
||||||
|
|
||||||
def __getitem__(self, key: str):
|
def __getitem__(self, key: str):
|
||||||
return self.local_state[key]
|
return self.local_state[key]
|
||||||
|
|
||||||
def __delitem__(self, key: str):
|
def __delitem__(self, key: str):
|
||||||
del self.local_state[key]
|
del self.local_state[key]
|
||||||
|
|
||||||
@ -303,7 +303,7 @@ class NodeStateLocal(NodeState):
|
|||||||
@comfytype(io_type="BOOLEAN")
|
@comfytype(io_type="BOOLEAN")
|
||||||
class Boolean(ComfyTypeIO):
|
class Boolean(ComfyTypeIO):
|
||||||
Type = bool
|
Type = bool
|
||||||
|
|
||||||
class Input(WidgetInputV3):
|
class Input(WidgetInputV3):
|
||||||
'''Boolean input.'''
|
'''Boolean input.'''
|
||||||
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
|
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
|
||||||
@ -313,7 +313,7 @@ class Boolean(ComfyTypeIO):
|
|||||||
self.label_on = label_on
|
self.label_on = label_on
|
||||||
self.label_off = label_off
|
self.label_off = label_off
|
||||||
self.default: bool
|
self.default: bool
|
||||||
|
|
||||||
def as_dict_V1(self):
|
def as_dict_V1(self):
|
||||||
return super().as_dict_V1() | prune_dict({
|
return super().as_dict_V1() | prune_dict({
|
||||||
"label_on": self.label_on,
|
"label_on": self.label_on,
|
||||||
@ -385,7 +385,7 @@ class String(ComfyTypeIO):
|
|||||||
self.multiline = multiline
|
self.multiline = multiline
|
||||||
self.placeholder = placeholder
|
self.placeholder = placeholder
|
||||||
self.default: str
|
self.default: str
|
||||||
|
|
||||||
def as_dict_V1(self):
|
def as_dict_V1(self):
|
||||||
return super().as_dict_V1() | prune_dict({
|
return super().as_dict_V1() | prune_dict({
|
||||||
"multiline": self.multiline,
|
"multiline": self.multiline,
|
||||||
@ -500,7 +500,7 @@ class Conditioning(ComfyTypeIO):
|
|||||||
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.
|
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.
|
||||||
|
|
||||||
(1024, 1024, 0, 0) would apply conditioning to the top-left 1024x1024 pixels.
|
(1024, 1024, 0, 0) would apply conditioning to the top-left 1024x1024 pixels.
|
||||||
|
|
||||||
("percentage", 0.5, 0.5, 0, 0) would apply conditioning to the top-left 50% of the image.''' # TODO: verify its actually top-left
|
("percentage", 0.5, 0.5, 0, 0) would apply conditioning to the top-left 50% of the image.''' # TODO: verify its actually top-left
|
||||||
strength: NotRequired[float]
|
strength: NotRequired[float]
|
||||||
'''Strength of conditioning. Default strength is 1.0.'''
|
'''Strength of conditioning. Default strength is 1.0.'''
|
||||||
@ -755,7 +755,7 @@ class MultiType:
|
|||||||
self.input_override.widgetType = self.input_override.get_io_type_V1()
|
self.input_override.widgetType = self.input_override.get_io_type_V1()
|
||||||
super().__init__(id, display_name, optional, tooltip, lazy, extra_dict)
|
super().__init__(id, display_name, optional, tooltip, lazy, extra_dict)
|
||||||
self._io_types = types
|
self._io_types = types
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def io_types(self) -> list[type[InputV3]]:
|
def io_types(self) -> list[type[InputV3]]:
|
||||||
'''
|
'''
|
||||||
@ -768,14 +768,14 @@ class MultiType:
|
|||||||
else:
|
else:
|
||||||
io_types.append(x)
|
io_types.append(x)
|
||||||
return io_types
|
return io_types
|
||||||
|
|
||||||
def get_io_type_V1(self):
|
def get_io_type_V1(self):
|
||||||
# ensure types are unique and order is preserved
|
# ensure types are unique and order is preserved
|
||||||
str_types = [x.io_type for x in self.io_types]
|
str_types = [x.io_type for x in self.io_types]
|
||||||
if self.input_override is not None:
|
if self.input_override is not None:
|
||||||
str_types.insert(0, self.input_override.get_io_type_V1())
|
str_types.insert(0, self.input_override.get_io_type_V1())
|
||||||
return ",".join(list(dict.fromkeys(str_types)))
|
return ",".join(list(dict.fromkeys(str_types)))
|
||||||
|
|
||||||
def as_dict_V1(self):
|
def as_dict_V1(self):
|
||||||
if self.input_override is not None:
|
if self.input_override is not None:
|
||||||
return self.input_override.as_dict_V1() | super().as_dict_V1()
|
return self.input_override.as_dict_V1() | super().as_dict_V1()
|
||||||
@ -870,7 +870,7 @@ class HiddenHolder:
|
|||||||
def __getattr__(self, key: str):
|
def __getattr__(self, key: str):
|
||||||
'''If hidden variable not found, return None.'''
|
'''If hidden variable not found, return None.'''
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, d: dict | None):
|
def from_dict(cls, d: dict | None):
|
||||||
if d is None:
|
if d is None:
|
||||||
@ -1088,7 +1088,7 @@ class ComfyNodeV3:
|
|||||||
|
|
||||||
RELATIVE_PYTHON_MODULE = None
|
RELATIVE_PYTHON_MODULE = None
|
||||||
SCHEMA = None
|
SCHEMA = None
|
||||||
|
|
||||||
# filled in during execution
|
# filled in during execution
|
||||||
state: NodeState = None
|
state: NodeState = None
|
||||||
resources: Resources = None
|
resources: Resources = None
|
||||||
@ -1097,28 +1097,24 @@ class ComfyNodeV3:
|
|||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def DEFINE_SCHEMA(cls) -> SchemaV3:
|
def DEFINE_SCHEMA(cls) -> SchemaV3:
|
||||||
"""
|
"""Override this function with one that returns a SchemaV3 instance."""
|
||||||
Override this function with one that returns a SchemaV3 instance.
|
raise NotImplementedError
|
||||||
"""
|
|
||||||
return None
|
|
||||||
DEFINE_SCHEMA = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def execute(cls, **kwargs) -> NodeOutput:
|
def execute(cls, **kwargs) -> NodeOutput:
|
||||||
pass
|
raise NotImplementedError
|
||||||
execute = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_inputs(cls, **kwargs) -> bool:
|
def validate_inputs(cls, **kwargs) -> bool:
|
||||||
"""Optionally, define this function to validate inputs; equivalnet to V1's VALIDATE_INPUTS."""
|
"""Optionally, define this function to validate inputs; equivalent to V1's VALIDATE_INPUTS."""
|
||||||
pass
|
raise NotImplementedError
|
||||||
validate_inputs = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fingerprint_inputs(cls, **kwargs) -> Any:
|
def fingerprint_inputs(cls, **kwargs) -> Any:
|
||||||
"""Optionally, define this function to fingerprint inputs; equivalent to V1's IS_CHANGED."""
|
"""Optionally, define this function to fingerprint inputs; equivalent to V1's IS_CHANGED."""
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
fingerprint_inputs = None
|
fingerprint_inputs = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -1135,8 +1131,8 @@ class ComfyNodeV3:
|
|||||||
|
|
||||||
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lazy_evaluation#defining-check-lazy-status
|
Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lazy_evaluation#defining-check-lazy-status
|
||||||
"""
|
"""
|
||||||
need = [name for name in kwargs if kwargs[name] is None]
|
return [name for name in kwargs if kwargs[name] is None]
|
||||||
return need
|
|
||||||
check_lazy_status = None
|
check_lazy_status = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -1405,7 +1401,7 @@ class NodeOutput:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> "NodeOutput":
|
def from_dict(cls, data: dict[str, Any]) -> "NodeOutput":
|
||||||
args = ()
|
args = ()
|
||||||
ui = None
|
ui = None
|
||||||
expand = None
|
expand = None
|
||||||
if "result" in data:
|
if "result" in data:
|
||||||
result = data["result"]
|
result = data["result"]
|
||||||
|
@ -32,7 +32,7 @@ class TorchDictFolderFilename(ResourceKey):
|
|||||||
class Resources(ABC):
|
class Resources(ABC):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
...
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get(self, key: ResourceKey, default: Any=...) -> Any:
|
def get(self, key: ResourceKey, default: Any=...) -> Any:
|
||||||
pass
|
pass
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
from comfy_api.v3.io import Image, Mask, FolderType, _UIOutput, ComfyNodeV3
|
from comfy_api.v3.io import Image, FolderType, _UIOutput, ComfyNodeV3
|
||||||
# used for image preview
|
# used for image preview
|
||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
import folder_paths
|
import folder_paths
|
||||||
@ -13,33 +12,33 @@ import json
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
class SavedResult:
|
class SavedResult(dict):
|
||||||
def __init__(self, filename: str, subfolder: str, type: FolderType):
|
def __init__(self, filename: str, subfolder: str, type: FolderType):
|
||||||
self.filename = filename
|
super().__init__(filename=filename, subfolder=subfolder,type=type.value)
|
||||||
self.subfolder = subfolder
|
|
||||||
self.type = type
|
@property
|
||||||
|
def filename(self) -> str:
|
||||||
def as_dict(self):
|
return self["filename"]
|
||||||
return {
|
|
||||||
"filename": self.filename,
|
@property
|
||||||
"subfolder": self.subfolder,
|
def subfolder(self) -> str:
|
||||||
"type": self.type
|
return self["subfolder"]
|
||||||
}
|
|
||||||
|
@property
|
||||||
|
def type(self) -> FolderType:
|
||||||
|
return FolderType(self["type"])
|
||||||
|
|
||||||
|
|
||||||
class PreviewImage(_UIOutput):
|
class PreviewImage(_UIOutput):
|
||||||
def __init__(self, image: Image.Type, animated: bool=False, cls: ComfyNodeV3=None, **kwargs):
|
def __init__(self, image: Image.Type, animated: bool=False, cls: ComfyNodeV3=None, **kwargs):
|
||||||
output_dir = folder_paths.get_temp_directory()
|
output_dir = folder_paths.get_temp_directory()
|
||||||
type = "temp"
|
|
||||||
prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
|
prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
|
||||||
compress_level = 1
|
filename_prefix = "ComfyUI" + prefix_append
|
||||||
filename_prefix = "ComfyUI"
|
|
||||||
|
|
||||||
filename_prefix += prefix_append
|
|
||||||
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])
|
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])
|
||||||
results = list()
|
results = list()
|
||||||
for (batch_number, image) in enumerate(image):
|
for (batch_number, image) in enumerate(image):
|
||||||
i = 255. * image.cpu().numpy()
|
img = PILImage.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8))
|
||||||
img = PILImage.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
||||||
metadata = None
|
metadata = None
|
||||||
if not args.disable_metadata and cls is not None:
|
if not args.disable_metadata and cls is not None:
|
||||||
metadata = PngInfo()
|
metadata = PngInfo()
|
||||||
@ -51,17 +50,16 @@ class PreviewImage(_UIOutput):
|
|||||||
|
|
||||||
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
||||||
file = f"{filename_with_batch_num}_{counter:05}_.png"
|
file = f"{filename_with_batch_num}_{counter:05}_.png"
|
||||||
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=compress_level)
|
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=1)
|
||||||
results.append(SavedResult(file, subfolder, type))
|
results.append(SavedResult(file, subfolder, FolderType.temp))
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
self.values = results
|
self.values = results
|
||||||
self.animated = animated
|
self.animated = animated
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
values = [x.as_dict() if isinstance(x, SavedResult) else x for x in self.values]
|
|
||||||
return {
|
return {
|
||||||
"images": values,
|
"images": self.values,
|
||||||
"animated": (self.animated,)
|
"animated": (self.animated,)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,36 +109,29 @@ class PreviewMask(PreviewImage):
|
|||||||
# comfy.utils.save_torch_file(output, file, metadata=metadata)
|
# comfy.utils.save_torch_file(output, file, metadata=metadata)
|
||||||
|
|
||||||
# self.values = values
|
# self.values = values
|
||||||
|
|
||||||
# def as_dict(self):
|
# def as_dict(self):
|
||||||
# values = [x.as_dict() if isinstance(x, SavedResult) else x for x in self.values]
|
|
||||||
# return {
|
# return {
|
||||||
# "latents": values,
|
# "latents": self.values,
|
||||||
# }
|
# }
|
||||||
|
|
||||||
class PreviewAudio(_UIOutput):
|
class PreviewAudio(_UIOutput):
|
||||||
def __init__(self, values: list[SavedResult | dict], **kwargs):
|
def __init__(self, values: list[SavedResult | dict], **kwargs):
|
||||||
self.values = values
|
self.values = values
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
values = [x.as_dict() if isinstance(x, SavedResult) else x for x in self.values]
|
return {"audio": self.values}
|
||||||
return {
|
|
||||||
"audio": values,
|
|
||||||
}
|
|
||||||
|
|
||||||
class PreviewUI3D(_UIOutput):
|
class PreviewUI3D(_UIOutput):
|
||||||
def __init__(self, values: list[SavedResult | dict], **kwargs):
|
def __init__(self, values: list[SavedResult | dict], **kwargs):
|
||||||
self.values = values
|
self.values = values
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
values = [x.as_dict() if isinstance(x, SavedResult) else x for x in self.values]
|
return {"3d": self.values}
|
||||||
return {
|
|
||||||
"3d": values,
|
|
||||||
}
|
|
||||||
|
|
||||||
class PreviewText(_UIOutput):
|
class PreviewText(_UIOutput):
|
||||||
def __init__(self, value: str, **kwargs):
|
def __init__(self, value: str, **kwargs):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
return {"text": (self.value,)}
|
return {"text": (self.value,)}
|
||||||
|
@ -20,7 +20,7 @@ class TestNode(ComfyNodeABC):
|
|||||||
"mask": (IO.MASK,),
|
"mask": (IO.MASK,),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = (IO.INT, IO.IMAGE)
|
RETURN_TYPES = (IO.INT, IO.IMAGE)
|
||||||
RETURN_NAMES = ("INT", "img🖼️")
|
RETURN_NAMES = ("INT", "img🖼️")
|
||||||
OUTPUT_TOOLTIPS = (None, "This is an image")
|
OUTPUT_TOOLTIPS = (None, "This is an image")
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import torch
|
import torch
|
||||||
import time
|
import time
|
||||||
from comfy_api.v3 import io, ui, resources
|
from comfy_api.v3 import io, ui, resources
|
||||||
import logging
|
import logging # noqa
|
||||||
import folder_paths
|
import folder_paths
|
||||||
import comfy.utils
|
import comfy.utils
|
||||||
import comfy.sd
|
import comfy.sd
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
@io.comfytype(io_type="XYZ")
|
@io.comfytype(io_type="XYZ")
|
||||||
class XYZ:
|
class XYZ:
|
||||||
@ -88,11 +88,11 @@ class V3TestNode(io.ComfyNodeV3):
|
|||||||
expected_int = 123
|
expected_int = 123
|
||||||
if "thing" not in cls.state:
|
if "thing" not in cls.state:
|
||||||
cls.state["thing"] = "hahaha"
|
cls.state["thing"] = "hahaha"
|
||||||
yyy = cls.state["thing"]
|
yyy = cls.state["thing"] # noqa
|
||||||
del cls.state["thing"]
|
del cls.state["thing"]
|
||||||
if cls.state.get_value("int2") is None:
|
if cls.state.get_value("int2") is None:
|
||||||
cls.state.set_value("int2", 123)
|
cls.state.set_value("int2", 123)
|
||||||
zzz = cls.state.get_value("int2")
|
zzz = cls.state.get_value("int2") # noqa
|
||||||
cls.state.pop("int2")
|
cls.state.pop("int2")
|
||||||
if cls.state.my_int is None:
|
if cls.state.my_int is None:
|
||||||
cls.state.my_int = expected_int
|
cls.state.my_int = expected_int
|
||||||
@ -175,7 +175,7 @@ class NInputsTest(io.ComfyNodeV3):
|
|||||||
io.Image.Output(),
|
io.Image.Output(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_inputs(cls, nmock, nmock2):
|
def validate_inputs(cls, nmock, nmock2):
|
||||||
return True
|
return True
|
||||||
|
@ -37,7 +37,7 @@ class SaveImage_V3(io.ComfyNodeV3):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, images, filename_prefix="ComfyUI"):
|
def execute(cls, images, filename_prefix="ComfyUI") -> io.NodeOutput:
|
||||||
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
||||||
filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]
|
filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]
|
||||||
)
|
)
|
||||||
@ -57,16 +57,122 @@ class SaveImage_V3(io.ComfyNodeV3):
|
|||||||
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
||||||
file = f"{filename_with_batch_num}_{counter:05}_.png"
|
file = f"{filename_with_batch_num}_{counter:05}_.png"
|
||||||
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4)
|
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4)
|
||||||
results.append({
|
results.append(ui.SavedResult(file, subfolder, io.FolderType.output))
|
||||||
"filename": file,
|
|
||||||
"subfolder": subfolder,
|
|
||||||
"type": "output",
|
|
||||||
})
|
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
return io.NodeOutput(ui={"images": results})
|
return io.NodeOutput(ui={"images": results})
|
||||||
|
|
||||||
|
|
||||||
|
class SaveAnimatedPNG_V3(io.ComfyNodeV3):
|
||||||
|
@classmethod
|
||||||
|
def DEFINE_SCHEMA(cls):
|
||||||
|
return io.SchemaV3(
|
||||||
|
node_id="SaveAnimatedPNG_V3",
|
||||||
|
display_name="Save Animated PNG _V3",
|
||||||
|
category="image/animation",
|
||||||
|
inputs=[
|
||||||
|
io.Image.Input("images"),
|
||||||
|
io.String.Input("filename_prefix", default="ComfyUI"),
|
||||||
|
io.Float.Input("fps", default=6.0, min=0.01, max=1000.0, step=0.01),
|
||||||
|
io.Int.Input("compress_level", default=4, min=0, max=9),
|
||||||
|
],
|
||||||
|
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
|
||||||
|
is_output_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, images, fps, compress_level, filename_prefix="ComfyUI") -> io.NodeOutput:
|
||||||
|
full_output_folder, filename, counter, subfolder, filename_prefix = (
|
||||||
|
folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0])
|
||||||
|
)
|
||||||
|
results = []
|
||||||
|
pil_images = []
|
||||||
|
for image in images:
|
||||||
|
img = Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8))
|
||||||
|
pil_images.append(img)
|
||||||
|
|
||||||
|
metadata = None
|
||||||
|
if not args.disable_metadata:
|
||||||
|
metadata = PngInfo()
|
||||||
|
if cls.hidden.prompt is not None:
|
||||||
|
metadata.add(
|
||||||
|
b"comf", "prompt".encode("latin-1", "strict") + b"\0" + json.dumps(cls.hidden.prompt).encode("latin-1", "strict"), after_idat=True
|
||||||
|
)
|
||||||
|
if cls.hidden.extra_pnginfo is not None:
|
||||||
|
for x in cls.hidden.extra_pnginfo:
|
||||||
|
metadata.add(
|
||||||
|
b"comf", x.encode("latin-1", "strict") + b"\0" + json.dumps(cls.hidden.extra_pnginfo[x]).encode("latin-1", "strict"), after_idat=True
|
||||||
|
)
|
||||||
|
|
||||||
|
file = f"{filename}_{counter:05}_.png"
|
||||||
|
pil_images[0].save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=compress_level, save_all=True, duration=int(1000.0/fps), append_images=pil_images[1:])
|
||||||
|
results.append(ui.SavedResult(file, subfolder, io.FolderType.output))
|
||||||
|
|
||||||
|
return io.NodeOutput(ui={"images": results, "animated": (True,) })
|
||||||
|
|
||||||
|
|
||||||
|
class SaveAnimatedWEBP_V3(io.ComfyNodeV3):
|
||||||
|
COMPRESS_METHODS = {"default": 4, "fastest": 0, "slowest": 6}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def DEFINE_SCHEMA(cls):
|
||||||
|
return io.SchemaV3(
|
||||||
|
node_id="SaveAnimatedWEBP_V3",
|
||||||
|
display_name="Save Animated WEBP _V3",
|
||||||
|
category="image/animation",
|
||||||
|
inputs=[
|
||||||
|
io.Image.Input("images"),
|
||||||
|
io.String.Input("filename_prefix", default="ComfyUI"),
|
||||||
|
io.Float.Input("fps", default=6.0, min=0.01, max=1000.0, step=0.01),
|
||||||
|
io.Boolean.Input("lossless", default=True),
|
||||||
|
io.Int.Input("quality", default=80, min=0, max=100),
|
||||||
|
io.Combo.Input("method", options=list(cls.COMPRESS_METHODS.keys())),
|
||||||
|
# "num_frames": ("INT", {"default": 0, "min": 0, "max": 8192}),
|
||||||
|
],
|
||||||
|
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
|
||||||
|
is_output_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, images, fps, filename_prefix, lossless, quality, method, num_frames=0) -> io.NodeOutput:
|
||||||
|
method = cls.COMPRESS_METHODS.get(method)
|
||||||
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0])
|
||||||
|
results = []
|
||||||
|
pil_images = []
|
||||||
|
for image in images:
|
||||||
|
img = Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8))
|
||||||
|
pil_images.append(img)
|
||||||
|
|
||||||
|
metadata = pil_images[0].getexif()
|
||||||
|
if not args.disable_metadata:
|
||||||
|
if cls.hidden.prompt is not None:
|
||||||
|
metadata[0x0110] = "prompt:{}".format(json.dumps(cls.hidden.prompt))
|
||||||
|
if cls.hidden.extra_pnginfo is not None:
|
||||||
|
inital_exif = 0x010f
|
||||||
|
for x in cls.hidden.extra_pnginfo:
|
||||||
|
metadata[inital_exif] = "{}:{}".format(x, json.dumps(cls.hidden.extra_pnginfo[x]))
|
||||||
|
inital_exif -= 1
|
||||||
|
|
||||||
|
if num_frames == 0:
|
||||||
|
num_frames = len(pil_images)
|
||||||
|
|
||||||
|
for i in range(0, len(pil_images), num_frames):
|
||||||
|
file = f"{filename}_{counter:05}_.webp"
|
||||||
|
pil_images[i].save(
|
||||||
|
os.path.join(full_output_folder, file),
|
||||||
|
save_all=True, duration=int(1000.0/fps),
|
||||||
|
append_images=pil_images[i + 1:i + num_frames],
|
||||||
|
exif=metadata,
|
||||||
|
lossless=lossless,
|
||||||
|
quality=quality,
|
||||||
|
method=method,
|
||||||
|
)
|
||||||
|
results.append(ui.SavedResult(file, subfolder, io.FolderType.output))
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
return io.NodeOutput(ui={"images": results, "animated": (num_frames != 1,)})
|
||||||
|
|
||||||
|
|
||||||
class PreviewImage_V3(io.ComfyNodeV3):
|
class PreviewImage_V3(io.ComfyNodeV3):
|
||||||
@classmethod
|
@classmethod
|
||||||
def DEFINE_SCHEMA(cls):
|
def DEFINE_SCHEMA(cls):
|
||||||
@ -76,17 +182,14 @@ class PreviewImage_V3(io.ComfyNodeV3):
|
|||||||
description="Preview the input images.",
|
description="Preview the input images.",
|
||||||
category="image",
|
category="image",
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Image.Input(
|
io.Image.Input("images", tooltip="The images to preview."),
|
||||||
"images",
|
|
||||||
tooltip="The images to preview.",
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
|
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
|
||||||
is_output_node=True,
|
is_output_node=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, images):
|
def execute(cls, images) -> io.NodeOutput:
|
||||||
return io.NodeOutput(ui=ui.PreviewImage(images, cls=cls))
|
return io.NodeOutput(ui=ui.PreviewImage(images, cls=cls))
|
||||||
|
|
||||||
|
|
||||||
@ -267,8 +370,9 @@ class LoadImageOutput_V3(io.ComfyNodeV3):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
NODES_LIST: list[type[io.ComfyNodeV3]] = [
|
NODES_LIST: list[type[io.ComfyNodeV3]] = [
|
||||||
|
SaveAnimatedPNG_V3,
|
||||||
|
SaveAnimatedWEBP_V3,
|
||||||
SaveImage_V3,
|
SaveImage_V3,
|
||||||
PreviewImage_V3,
|
PreviewImage_V3,
|
||||||
LoadImage_V3,
|
LoadImage_V3,
|
||||||
|
50
execution.py
50
execution.py
@ -28,7 +28,7 @@ from comfy_execution.graph import (
|
|||||||
)
|
)
|
||||||
from comfy_execution.graph_utils import GraphBuilder, is_link
|
from comfy_execution.graph_utils import GraphBuilder, is_link
|
||||||
from comfy_execution.validation import validate_node_input
|
from comfy_execution.validation import validate_node_input
|
||||||
from comfy_api.v3.io import NodeOutput, ComfyNodeV3, Hidden, NodeStateLocal, ResourcesLocal, AutogrowDynamic, is_class, lock_class
|
from comfy_api.v3 import io, helpers
|
||||||
|
|
||||||
|
|
||||||
class ExecutionResult(Enum):
|
class ExecutionResult(Enum):
|
||||||
@ -54,7 +54,7 @@ class IsChangedCache:
|
|||||||
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
||||||
has_is_changed = False
|
has_is_changed = False
|
||||||
is_changed_name = None
|
is_changed_name = None
|
||||||
if issubclass(class_def, ComfyNodeV3) and getattr(class_def, "fingerprint_inputs", None) is not None:
|
if issubclass(class_def, io.ComfyNodeV3) and getattr(class_def, "fingerprint_inputs", None) is not None:
|
||||||
has_is_changed = True
|
has_is_changed = True
|
||||||
is_changed_name = "fingerprint_inputs"
|
is_changed_name = "fingerprint_inputs"
|
||||||
elif hasattr(class_def, "IS_CHANGED"):
|
elif hasattr(class_def, "IS_CHANGED"):
|
||||||
@ -127,7 +127,7 @@ class CacheSet:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def get_input_data(inputs, class_def, unique_id, outputs=None, dynprompt=None, extra_data={}):
|
def get_input_data(inputs, class_def, unique_id, outputs=None, dynprompt=None, extra_data={}):
|
||||||
is_v3 = issubclass(class_def, ComfyNodeV3)
|
is_v3 = issubclass(class_def, io.ComfyNodeV3)
|
||||||
if is_v3:
|
if is_v3:
|
||||||
valid_inputs, schema = class_def.INPUT_TYPES(include_hidden=False, return_schema=True)
|
valid_inputs, schema = class_def.INPUT_TYPES(include_hidden=False, return_schema=True)
|
||||||
else:
|
else:
|
||||||
@ -161,18 +161,18 @@ def get_input_data(inputs, class_def, unique_id, outputs=None, dynprompt=None, e
|
|||||||
|
|
||||||
if is_v3:
|
if is_v3:
|
||||||
if schema.hidden:
|
if schema.hidden:
|
||||||
if Hidden.prompt in schema.hidden:
|
if io.Hidden.prompt in schema.hidden:
|
||||||
hidden_inputs_v3[Hidden.prompt] = dynprompt.get_original_prompt() if dynprompt is not None else {}
|
hidden_inputs_v3[io.Hidden.prompt] = dynprompt.get_original_prompt() if dynprompt is not None else {}
|
||||||
if Hidden.dynprompt in schema.hidden:
|
if io.Hidden.dynprompt in schema.hidden:
|
||||||
hidden_inputs_v3[Hidden.dynprompt] = dynprompt
|
hidden_inputs_v3[io.Hidden.dynprompt] = dynprompt
|
||||||
if Hidden.extra_pnginfo in schema.hidden:
|
if io.Hidden.extra_pnginfo in schema.hidden:
|
||||||
hidden_inputs_v3[Hidden.extra_pnginfo] = extra_data.get('extra_pnginfo', None)
|
hidden_inputs_v3[io.Hidden.extra_pnginfo] = extra_data.get('extra_pnginfo', None)
|
||||||
if Hidden.unique_id in schema.hidden:
|
if io.Hidden.unique_id in schema.hidden:
|
||||||
hidden_inputs_v3[Hidden.unique_id] = unique_id
|
hidden_inputs_v3[io.Hidden.unique_id] = unique_id
|
||||||
if Hidden.auth_token_comfy_org in schema.hidden:
|
if io.Hidden.auth_token_comfy_org in schema.hidden:
|
||||||
hidden_inputs_v3[Hidden.auth_token_comfy_org] = extra_data.get("auth_token_comfy_org", None)
|
hidden_inputs_v3[io.Hidden.auth_token_comfy_org] = extra_data.get("auth_token_comfy_org", None)
|
||||||
if Hidden.api_key_comfy_org in schema.hidden:
|
if io.Hidden.api_key_comfy_org in schema.hidden:
|
||||||
hidden_inputs_v3[Hidden.api_key_comfy_org] = extra_data.get("api_key_comfy_org", None)
|
hidden_inputs_v3[io.Hidden.api_key_comfy_org] = extra_data.get("api_key_comfy_org", None)
|
||||||
else:
|
else:
|
||||||
if "hidden" in valid_inputs:
|
if "hidden" in valid_inputs:
|
||||||
h = valid_inputs["hidden"]
|
h = valid_inputs["hidden"]
|
||||||
@ -224,9 +224,9 @@ def _map_node_over_list(obj, input_data_all, func, allow_interrupt=False, execut
|
|||||||
if pre_execute_cb is not None and index is not None:
|
if pre_execute_cb is not None and index is not None:
|
||||||
pre_execute_cb(index)
|
pre_execute_cb(index)
|
||||||
# V3
|
# V3
|
||||||
if isinstance(obj, ComfyNodeV3) or (is_class(obj) and issubclass(obj, ComfyNodeV3)):
|
if isinstance(obj, io.ComfyNodeV3) or (io.is_class(obj) and issubclass(obj, io.ComfyNodeV3)):
|
||||||
# if is just a class, then assign no resources or state, just create clone
|
# if is just a class, then assign no resources or state, just create clone
|
||||||
if is_class(obj):
|
if io.is_class(obj):
|
||||||
type_obj = obj
|
type_obj = obj
|
||||||
obj.VALIDATE_CLASS()
|
obj.VALIDATE_CLASS()
|
||||||
class_clone = obj.PREPARE_CLASS_CLONE(hidden_inputs)
|
class_clone = obj.PREPARE_CLASS_CLONE(hidden_inputs)
|
||||||
@ -238,16 +238,16 @@ def _map_node_over_list(obj, input_data_all, func, allow_interrupt=False, execut
|
|||||||
# NOTE: this is a mock of state management; for local, just stores NodeStateLocal on node instance
|
# NOTE: this is a mock of state management; for local, just stores NodeStateLocal on node instance
|
||||||
if hasattr(obj, "local_state"):
|
if hasattr(obj, "local_state"):
|
||||||
if obj.local_state is None:
|
if obj.local_state is None:
|
||||||
obj.local_state = NodeStateLocal(class_clone.hidden.unique_id)
|
obj.local_state = io.NodeStateLocal(class_clone.hidden.unique_id)
|
||||||
class_clone.state = obj.local_state
|
class_clone.state = obj.local_state
|
||||||
# NOTE: this is a mock of resource management; for local, just stores ResourcesLocal on node instance
|
# NOTE: this is a mock of resource management; for local, just stores ResourcesLocal on node instance
|
||||||
if hasattr(obj, "local_resources"):
|
if hasattr(obj, "local_resources"):
|
||||||
if obj.local_resources is None:
|
if obj.local_resources is None:
|
||||||
obj.local_resources = ResourcesLocal()
|
obj.local_resources = io.ResourcesLocal()
|
||||||
class_clone.resources = obj.local_resources
|
class_clone.resources = obj.local_resources
|
||||||
# TODO: delete this when done testing mocking dynamic inputs
|
# TODO: delete this when done testing mocking dynamic inputs
|
||||||
for si in obj.SCHEMA.inputs:
|
for si in obj.SCHEMA.inputs:
|
||||||
if isinstance(si, AutogrowDynamic.Input):
|
if isinstance(si, io.AutogrowDynamic.Input):
|
||||||
add_key = si.id
|
add_key = si.id
|
||||||
dynamic_list = []
|
dynamic_list = []
|
||||||
real_inputs = {k: v for k, v in inputs.items()}
|
real_inputs = {k: v for k, v in inputs.items()}
|
||||||
@ -255,7 +255,7 @@ def _map_node_over_list(obj, input_data_all, func, allow_interrupt=False, execut
|
|||||||
dynamic_list.append(real_inputs.pop(d.id, None))
|
dynamic_list.append(real_inputs.pop(d.id, None))
|
||||||
dynamic_list = [x for x in dynamic_list if x is not None]
|
dynamic_list = [x for x in dynamic_list if x is not None]
|
||||||
inputs = {**real_inputs, add_key: dynamic_list}
|
inputs = {**real_inputs, add_key: dynamic_list}
|
||||||
results.append(getattr(type_obj, func).__func__(lock_class(class_clone), **inputs))
|
results.append(getattr(type_obj, func).__func__(io.lock_class(class_clone), **inputs))
|
||||||
# V1
|
# V1
|
||||||
else:
|
else:
|
||||||
results.append(getattr(obj, func)(**inputs))
|
results.append(getattr(obj, func)(**inputs))
|
||||||
@ -318,7 +318,7 @@ def get_output_data(obj, input_data_all, execution_block_cb=None, pre_execute_cb
|
|||||||
result = tuple([result] * len(obj.RETURN_TYPES))
|
result = tuple([result] * len(obj.RETURN_TYPES))
|
||||||
results.append(result)
|
results.append(result)
|
||||||
subgraph_results.append((None, result))
|
subgraph_results.append((None, result))
|
||||||
elif isinstance(r, NodeOutput):
|
elif isinstance(r, io.NodeOutput):
|
||||||
# V3
|
# V3
|
||||||
if r.ui is not None:
|
if r.ui is not None:
|
||||||
if isinstance(r.ui, dict):
|
if isinstance(r.ui, dict):
|
||||||
@ -670,11 +670,9 @@ def validate_inputs(prompt, item, validated):
|
|||||||
|
|
||||||
validate_function_inputs = []
|
validate_function_inputs = []
|
||||||
validate_has_kwargs = False
|
validate_has_kwargs = False
|
||||||
validate_function_name = None
|
if issubclass(obj_class, io.ComfyNodeV3):
|
||||||
validate_function = None
|
|
||||||
if issubclass(obj_class, ComfyNodeV3):
|
|
||||||
validate_function_name = "validate_inputs"
|
validate_function_name = "validate_inputs"
|
||||||
validate_function = getattr(obj_class, validate_function_name, None)
|
validate_function = helpers.first_real_override(obj_class, validate_function_name, base=io.ComfyNodeV3)
|
||||||
else:
|
else:
|
||||||
validate_function_name = "VALIDATE_INPUTS"
|
validate_function_name = "VALIDATE_INPUTS"
|
||||||
validate_function = getattr(obj_class, validate_function_name, None)
|
validate_function = getattr(obj_class, validate_function_name, None)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user