Refactored v3 code so that v3_01 becomes v3, v3_01 is deleted since no longer necessary

This commit is contained in:
Jedrzej Kosinski 2025-06-18 23:29:32 -05:00
parent 38721fdb64
commit f9aec12ef1
6 changed files with 429 additions and 1650 deletions

View File

@ -1,20 +1,13 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Literal, TYPE_CHECKING, TypeVar from typing import Any, Literal, TYPE_CHECKING, TypeVar, Callable, Optional, cast
from enum import Enum from enum import Enum
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from comfy.comfy_types.node_typing import IO from comfy.comfy_types.node_typing import IO
# if TYPE_CHECKING:
import torch import torch
class InputBehavior(str, Enum):
'''Likely deprecated; required/optional can be a bool, unlikely to be more categories that fit.'''
required = "required"
optional = "optional"
class FolderType(str, Enum): class FolderType(str, Enum):
input = "input" input = "input"
output = "output" output = "output"
@ -55,26 +48,111 @@ def is_class(obj):
return isinstance(obj, type) return isinstance(obj, type)
def copy_class(cls: type) -> type:
'''
Copy a class and its attributes.
'''
if cls is None:
return None
cls_dict = {
k: v for k, v in cls.__dict__.items()
if k not in ('__dict__', '__weakref__', '__module__', '__doc__')
}
# new class
new_cls = type(
cls.__name__,
(cls,),
cls_dict
)
# metadata preservation
new_cls.__module__ = cls.__module__
new_cls.__doc__ = cls.__doc__
return new_cls
class NumberDisplay(str, Enum): class NumberDisplay(str, Enum):
number = "number" number = "number"
slider = "slider" slider = "slider"
class ComfyType:
Type = Any
io_type: str = None
Input: type[InputV3] = None
Output: type[OutputV3] = None
# NOTE: this is a workaround to make the decorator return the correct type
T = TypeVar("T", bound=type)
def comfytype(io_type: str, **kwargs):
'''
Decorator to mark nested classes as ComfyType; io_type will be bound to the class.
A ComfyType may have the following attributes:
- Type = <type hint here>
- class Input(InputV3): ...
- class Output(OutputV3): ...
'''
def decorator(cls: T) -> T:
if isinstance(cls, ComfyType) or issubclass(cls, ComfyType):
# clone Input and Output classes to avoid modifying the original class
new_cls = cls
new_cls.Input = copy_class(new_cls.Input)
new_cls.Output = copy_class(new_cls.Output)
else:
# copy class attributes except for special ones that shouldn't be in type()
cls_dict = {
k: v for k, v in cls.__dict__.items()
if k not in ('__dict__', '__weakref__', '__module__', '__doc__')
}
# new class
new_cls: ComfyType = type(
cls.__name__,
(cls, ComfyType),
cls_dict
)
# metadata preservation
new_cls.__module__ = cls.__module__
new_cls.__doc__ = cls.__doc__
# assign ComfyType attributes, if needed
# NOTE: do we need __ne__ trick for io_type? (see IO.__ne__ for details)
new_cls.io_type = io_type
if new_cls.Input is not None:
new_cls.Input.Parent = new_cls
if new_cls.Output is not None:
new_cls.Output.Parent = new_cls
return new_cls
return decorator
def Custom(io_type: IO | str) -> type[ComfyType]:
'''Create a ComfyType for a custom io_type.'''
@comfytype(io_type=io_type)
class CustomComfyType(ComfyTypeIO):
...
return CustomComfyType
class IO_V3: class IO_V3:
''' '''
Base class for V3 Inputs and Outputs. Base class for V3 Inputs and Outputs.
''' '''
Type = Any Parent: ComfyType = None
def __init__(self): def __init__(self):
pass pass
def __init_subclass__(cls, io_type: IO | str, **kwargs): # def __init_subclass__(cls, io_type: IO | str, **kwargs):
# TODO: do we need __ne__ trick for io_type? (see IO.__ne__ for details) # # TODO: do we need __ne__ trick for io_type? (see IO.__ne__ for details)
cls.io_type = io_type # cls.io_type = io_type
super().__init_subclass__(**kwargs) # super().__init_subclass__(**kwargs)
@property
def io_type(self):
return self.Parent.io_type
class InputV3(IO_V3, io_type=None): @property
def Type(self):
return self.Parent.Type
class InputV3(IO_V3):
''' '''
Base class for a V3 Input. Base class for a V3 Input.
''' '''
@ -96,7 +174,7 @@ class InputV3(IO_V3, io_type=None):
def get_io_type_V1(self): def get_io_type_V1(self):
return self.io_type return self.io_type
class WidgetInputV3(InputV3, io_type=None): class WidgetInputV3(InputV3):
''' '''
Base class for a V3 Input with widget. Base class for a V3 Input with widget.
''' '''
@ -115,7 +193,7 @@ class WidgetInputV3(InputV3, io_type=None):
"widgetType": self.widgetType, "widgetType": self.widgetType,
}) })
class OutputV3(IO_V3, io_type=None): class OutputV3(IO_V3):
def __init__(self, id: str, display_name: str=None, tooltip: str=None, def __init__(self, id: str, display_name: str=None, tooltip: str=None,
is_output_list=False): is_output_list=False):
self.id = id self.id = id
@ -123,414 +201,334 @@ class OutputV3(IO_V3, io_type=None):
self.tooltip = tooltip self.tooltip = tooltip
self.is_output_list = is_output_list self.is_output_list = is_output_list
def CustomType(io_type: IO | str) -> type[IO_V3]:
name = f"{io_type}_IO_V3"
return type(name, (IO_V3,), {}, io_type=io_type)
def CustomInput(id: str, io_type: IO | str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None) -> InputV3: class ComfyTypeIO(ComfyType):
''' '''ComfyType subclass that has default Input and Output classes; useful for basic Inputs and Outputs.'''
Defines input for 'io_type'. Can be used to stand in for non-core types. class Input(InputV3):
''' ...
input_kwargs = { class Output(OutputV3):
"id": id, ...
"display_name": display_name,
"optional": optional,
"tooltip": tooltip,
"lazy": lazy,
}
return type(f"{io_type}Input", (InputV3,), {}, io_type=io_type)(**input_kwargs)
def CustomOutput(id: str, io_type: IO | str, display_name: str=None, tooltip: str=None) -> OutputV3:
'''
Defines output for 'io_type'. Can be used to stand in for non-core types.
'''
input_kwargs = {
"id": id,
"display_name": display_name,
"tooltip": tooltip,
}
return type(f"{io_type}Output", (OutputV3,), {}, io_type=io_type)(**input_kwargs)
class BooleanInput(WidgetInputV3, io_type=IO.BOOLEAN): class NodeState:
''' def __init__(self, node_id: str):
Boolean input. self.node_id = node_id
'''
class NodeStateLocal(NodeState):
def __init__(self, node_id: str):
super().__init__(node_id)
self.local_state = {}
def __getattr__(self, key: str):
local_state = type(self).__getattribute__(self, "local_state")
if key in local_state:
return local_state[key]
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{key}'")
def __setattr__(self, key: str, value: Any):
if key in ['node_id', 'local_state']:
super().__setattr__(key, value)
else:
self.local_state[key] = value
@comfytype(io_type=IO.BOOLEAN)
class Boolean:
Type = bool Type = bool
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
default: bool=None, label_on: str=None, label_off: str=None,
socketless: bool=None, widgetType: str=None):
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType)
self.label_on = label_on
self.label_off = label_off
self.default: bool
def as_dict_V1(self): class Input(WidgetInputV3):
return super().as_dict_V1() | prune_dict({ '''Boolean input.'''
"label_on": self.label_on, def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
"label_off": self.label_off, default: bool=None, label_on: str=None, label_off: str=None,
}) socketless: bool=None, widgetType: str=None):
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType)
self.label_on = label_on
self.label_off = label_off
self.default: bool
def as_dict_V1(self):
return super().as_dict_V1() | prune_dict({
"label_on": self.label_on,
"label_off": self.label_off,
})
class BooleanOutput(OutputV3, io_type=IO.BOOLEAN): class Output(OutputV3):
... ...
class IntegerInput(WidgetInputV3, io_type=IO.INT): @comfytype(io_type=IO.INT)
''' class Int:
Integer input.
'''
Type = int Type = int
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
default: int=None, min: int=None, max: int=None, step: int=None, control_after_generate: bool=None,
display_mode: NumberDisplay=None, socketless: bool=None, widgetType: str=None):
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType)
self.min = min
self.max = max
self.step = step
self.control_after_generate = control_after_generate
self.display_mode = display_mode
self.default: int
def as_dict_V1(self): class Input(WidgetInputV3):
return super().as_dict_V1() | prune_dict({ '''Integer input.'''
"min": self.min, def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
"max": self.max, default: int=None, min: int=None, max: int=None, step: int=None, control_after_generate: bool=None,
"step": self.step, display_mode: NumberDisplay=None, socketless: bool=None, widgetType: str=None):
"control_after_generate": self.control_after_generate, super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType)
"display": self.display_mode, # NOTE: in frontend, the parameter is called "display" self.min = min
}) self.max = max
self.step = step
self.control_after_generate = control_after_generate
self.display_mode = display_mode
self.default: int
def as_dict_V1(self):
return super().as_dict_V1() | prune_dict({
"min": self.min,
"max": self.max,
"step": self.step,
"control_after_generate": self.control_after_generate,
"display": self.display_mode,
})
class IntegerOutput(OutputV3, io_type=IO.INT): class Output(OutputV3):
... ...
class FloatInput(WidgetInputV3, io_type=IO.FLOAT): @comfytype(io_type=IO.FLOAT)
''' class Float:
Float input.
'''
Type = float Type = float
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
default: float=None, min: float=None, max: float=None, step: float=None, round: float=None,
display_mode: NumberDisplay=None, socketless: bool=None, widgetType: str=None):
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType)
self.default = default
self.min = min
self.max = max
self.step = step
self.round = round
self.display_mode = display_mode
self.default: float
def as_dict_V1(self): class Input(WidgetInputV3):
return super().as_dict_V1() | prune_dict({ '''Float input.'''
"min": self.min, def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
"max": self.max, default: float=None, min: float=None, max: float=None, step: float=None, round: float=None,
"step": self.step, display_mode: NumberDisplay=None, socketless: bool=None, widgetType: str=None):
"round": self.round, super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType)
"display": self.display_mode, # NOTE: in frontend, the parameter is called "display" self.default = default
}) self.min = min
self.max = max
self.step = step
self.round = round
self.display_mode = display_mode
self.default: float
def as_dict_V1(self):
return super().as_dict_V1() | prune_dict({
"min": self.min,
"max": self.max,
"step": self.step,
"round": self.round,
"display": self.display_mode,
})
class FloatOutput(OutputV3, io_type=IO.FLOAT): class Output(OutputV3):
... ...
class StringInput(WidgetInputV3, io_type=IO.STRING): @comfytype(io_type=IO.STRING)
''' class String:
String input.
'''
Type = str Type = str
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
multiline=False, placeholder: str=None, default: int=None,
socketless: bool=None, widgetType: str=None):
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType)
self.multiline = multiline
self.placeholder = placeholder
self.default: str
def as_dict_V1(self): class Input(WidgetInputV3):
return super().as_dict_V1() | prune_dict({ '''String input.'''
"multiline": self.multiline, def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
"placeholder": self.placeholder, multiline=False, placeholder: str=None, default: int=None,
}) socketless: bool=None, widgetType: str=None):
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType)
self.multiline = multiline
self.placeholder = placeholder
self.default: str
def as_dict_V1(self):
return super().as_dict_V1() | prune_dict({
"multiline": self.multiline,
"placeholder": self.placeholder,
})
class StringOutput(OutputV3, io_type=IO.STRING): class Output(OutputV3):
... ...
class ComboInput(WidgetInputV3, io_type=IO.COMBO): @comfytype(io_type=IO.COMBO)
'''Combo input (dropdown).''' class Combo:
Type = str Type = str
def __init__(self, id: str, options: list[str]=None, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None, class Input(WidgetInputV3):
default: str=None, control_after_generate: bool=None, '''Combo input (dropdown).'''
image_upload: bool=None, image_folder: FolderType=None, Type = str
remote: RemoteOptions=None, def __init__(self, id: str, options: list[str]=None, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
socketless: bool=None, widgetType: str=None): default: str=None, control_after_generate: bool=None,
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType) image_upload: bool=None, image_folder: FolderType=None,
self.multiselect = False remote: RemoteOptions=None,
self.options = options socketless: bool=None, widgetType: str=None):
self.control_after_generate = control_after_generate super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, widgetType)
self.image_upload = image_upload self.multiselect = False
self.image_folder = image_folder self.options = options
self.remote = remote self.control_after_generate = control_after_generate
self.default: str self.image_upload = image_upload
self.image_folder = image_folder
def as_dict_V1(self): self.remote = remote
return super().as_dict_V1() | prune_dict({ self.default: str
"multiselect": self.multiselect,
"options": self.options, def as_dict_V1(self):
"control_after_generate": self.control_after_generate, return super().as_dict_V1() | prune_dict({
"image_upload": self.image_upload, "multiselect": self.multiselect,
"image_folder": self.image_folder.value if self.image_folder else None, "options": self.options,
"remote": self.remote.as_dict() if self.remote else None, "control_after_generate": self.control_after_generate,
}) "image_upload": self.image_upload,
"image_folder": self.image_folder.value if self.image_folder else None,
"remote": self.remote.as_dict() if self.remote else None,
})
class MultiselectComboWidget(ComboInput, io_type=IO.COMBO): @comfytype(io_type=IO.COMBO)
class MultiCombo:
'''Multiselect Combo input (dropdown for selecting potentially more than one value).''' '''Multiselect Combo input (dropdown for selecting potentially more than one value).'''
def __init__(self, id: str, options: list[str], display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None, Type = list[str]
default: list[str]=None, placeholder: str=None, chip: bool=None, control_after_generate: bool=None, class Input(Combo.Input):
socketless: bool=None, widgetType: str=None): def __init__(self, id: str, options: list[str], display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
super().__init__(id, options, display_name, optional, tooltip, lazy, default, control_after_generate, socketless, widgetType) default: list[str]=None, placeholder: str=None, chip: bool=None, control_after_generate: bool=None,
self.multiselect = True socketless: bool=None, widgetType: str=None):
self.placeholder = placeholder super().__init__(id, options, display_name, optional, tooltip, lazy, default, control_after_generate, socketless, widgetType)
self.chip = chip self.multiselect = True
self.default: list[str] self.placeholder = placeholder
self.chip = chip
def as_dict_V1(self): self.default: list[str]
return super().as_dict_V1() | prune_dict({
"multiselect": self.multiselect, def as_dict_V1(self):
"placeholder": self.placeholder, return super().as_dict_V1() | prune_dict({
"chip": self.chip, "multiselect": self.multiselect,
}) "placeholder": self.placeholder,
"chip": self.chip,
})
class ImageInput(InputV3, io_type=IO.IMAGE):
'''Image input.''' @comfytype(io_type=IO.IMAGE)
class Image(ComfyTypeIO):
Type = torch.Tensor Type = torch.Tensor
class ImageOutput(OutputV3, io_type=IO.IMAGE): @comfytype(io_type=IO.MASK)
'''Image output.''' class Mask(ComfyTypeIO):
Type = torch.Tensor Type = torch.Tensor
class MaskInput(InputV3, io_type=IO.MASK): @comfytype(io_type=IO.LATENT)
'''Mask input.''' class Latent(ComfyTypeIO):
Type = torch.Tensor Type = Any # TODO: make Type a TypedDict
class MaskOutput(OutputV3, io_type=IO.MASK): @comfytype(io_type=IO.CONDITIONING)
'''Mask output.''' class Conditioning(ComfyTypeIO):
Type = torch.Tensor Type = Any
class LatentInput(InputV3, io_type=IO.LATENT): @comfytype(io_type=IO.SAMPLER)
'''Latent input.''' class Sampler(ComfyTypeIO):
# TODO: make Type a TypedDict Type = Any
...
class LatentOutput(OutputV3, io_type=IO.LATENT): @comfytype(io_type=IO.SIGMAS)
'''Latent output.''' class Sigmas(ComfyTypeIO):
# TODO: make Type a TypedDict Type = Any
...
class ConditioningInput(InputV3, io_type=IO.CONDITIONING): @comfytype(io_type=IO.NOISE)
'''Conditioning input.''' class Noise(ComfyTypeIO):
# TODO: make Type a TypedDict Type = Any
...
class ConditioningOutput(OutputV3, io_type=IO.CONDITIONING): @comfytype(io_type=IO.GUIDER)
'''Conditioning output.''' class Guider(ComfyTypeIO):
# TODO: make Type a TypedDict Type = Any
...
class SamplerInput(InputV3, io_type=IO.SAMPLER): @comfytype(io_type=IO.CLIP)
'''Sampler input.''' class Clip(ComfyTypeIO):
... Type = Any
class SamplerOutput(OutputV3, io_type=IO.SAMPLER): @comfytype(io_type=IO.CONTROL_NET)
'''Sampler output.''' class ControlNet(ComfyTypeIO):
... Type = Any
class SigmasInput(InputV3, io_type=IO.SIGMAS): @comfytype(io_type=IO.VAE)
'''Sigmas input.''' class Vae(ComfyTypeIO):
... Type = Any
class SigmasOutput(OutputV3, io_type=IO.SIGMAS): @comfytype(io_type=IO.MODEL)
'''Sigmas output.''' class Model(ComfyTypeIO):
... Type = Any
class GuiderInput(InputV3, io_type=IO.GUIDER): @comfytype(io_type=IO.CLIP_VISION)
'''Guider input.''' class ClipVision(ComfyTypeIO):
... Type = Any
class GuiderOutput(OutputV3, io_type=IO.GUIDER): @comfytype(io_type=IO.CLIP_VISION_OUTPUT)
'''Guider output.''' class ClipVisionOutput(ComfyTypeIO):
... Type = Any
class NoiseInput(InputV3, io_type=IO.NOISE): @comfytype(io_type=IO.STYLE_MODEL)
'''Noise input.''' class StyleModel(ComfyTypeIO):
... Type = Any
class NoiseOutput(OutputV3, io_type=IO.NOISE): @comfytype(io_type=IO.GLIGEN)
'''Noise output.''' class Gligen(ComfyTypeIO):
... Type = Any
class ClipInput(InputV3, io_type=IO.CLIP): @comfytype(io_type=IO.UPSCALE_MODEL)
'''Clip input.''' class UpscaleModel(ComfyTypeIO):
... Type = Any
class ClipOutput(OutputV3, io_type=IO.CLIP): @comfytype(io_type=IO.AUDIO)
'''Clip output.''' class Audio(ComfyTypeIO):
... Type = Any
class ControlNetInput(InputV3, io_type=IO.CONTROL_NET): @comfytype(io_type=IO.POINT)
'''ControlNet input.''' class Point(ComfyTypeIO):
... Type = Any
class ControlNetOutput(OutputV3, io_type=IO.CONTROL_NET): @comfytype(io_type=IO.FACE_ANALYSIS)
'''ControlNet output.''' class FaceAnalysis(ComfyTypeIO):
... Type = Any
class VaeInput(InputV3, io_type=IO.VAE): @comfytype(io_type=IO.BBOX)
'''Vae input.''' class BBOX(ComfyTypeIO):
... Type = Any
class VaeOutput(OutputV3, io_type=IO.VAE): @comfytype(io_type=IO.SEGS)
'''Vae output.''' class SEGS(ComfyTypeIO):
... Type = Any
class ModelInput(InputV3, io_type=IO.MODEL): @comfytype(io_type=IO.VIDEO)
'''Model input.''' class Video(ComfyTypeIO):
... Type = Any
class ModelOutput(OutputV3, io_type=IO.MODEL): @comfytype(io_type="COMFY_MULTITYPED_V3")
'''Model output.''' class MultiType:
... Type = Any
class Input(InputV3):
class ClipVisionInput(InputV3, io_type=IO.CLIP_VISION):
'''ClipVision input.'''
...
class ClipVisionOutput(OutputV3, io_type=IO.CLIP_VISION):
'''ClipVision output.'''
...
class ClipVisionOutputInput(InputV3, io_type=IO.CLIP_VISION_OUTPUT):
'''CLipVisionOutput input.'''
...
class ClipVisionOutputOutput(OutputV3, io_type=IO.CLIP_VISION_OUTPUT):
'''CLipVisionOutput output.'''
...
class StyleModelInput(InputV3, io_type=IO.STYLE_MODEL):
'''StyleModel input.'''
...
class StyleModelOutput(OutputV3, io_type=IO.STYLE_MODEL):
'''StyleModel output.'''
...
class GligenInput(InputV3, io_type=IO.GLIGEN):
'''Gligen input.'''
...
class GligenOutput(OutputV3, io_type=IO.GLIGEN):
'''Gligen output.'''
...
class UpscaleModelInput(InputV3, io_type=IO.UPSCALE_MODEL):
'''UpscaleModel input.'''
...
class UpscaleModelOutput(OutputV3, io_type=IO.UPSCALE_MODEL):
'''UpscaleModel output.'''
...
class AudioInput(InputV3, io_type=IO.AUDIO):
'''Audio input.'''
...
class AudioOutput(OutputV3, io_type=IO.AUDIO):
'''Audio output.'''
...
class PointInput(InputV3, io_type=IO.POINT):
'''Point input.'''
...
class PointOutput(OutputV3, io_type=IO.POINT):
'''Point output.'''
...
class FaceAnalysisInput(InputV3, io_type=IO.FACE_ANALYSIS):
'''FaceAnalysis input.'''
...
class FaceAnalysisOutput(OutputV3, io_type=IO.FACE_ANALYSIS):
'''FaceAnalysis output.'''
...
class BBOXInput(InputV3, io_type=IO.BBOX):
'''Bbox input.'''
...
class BBOXOutput(OutputV3, io_type=IO.BBOX):
'''Bbox output.'''
...
class SEGSInput(InputV3, io_type=IO.SEGS):
'''SEGS input.'''
...
class SEGSOutput(OutputV3, io_type=IO.SEGS):
'''SEGS output.'''
...
class VideoInput(InputV3, io_type=IO.VIDEO):
'''Video input.'''
...
class VideoOutput(OutputV3, io_type=IO.VIDEO):
'''Video output.'''
...
class MultitypedInput(InputV3, io_type="COMFY_MULTITYPED_V3"):
'''
Input that permits more than one input type.
'''
def __init__(self, id: str, io_types: list[type[IO_V3] | InputV3 | IO |str], display_name: str=None, optional=False, tooltip: str=None,):
super().__init__(id, display_name, optional, tooltip)
self._io_types = io_types
@property
def io_types(self) -> list[type[InputV3]]:
''' '''
Returns list of InputV3 class types permitted. Input that permits more than one input type.
''' '''
io_types = [] def __init__(self, id: str, io_types: list[type[ComfyType] | ComfyType | IO |str], display_name: str=None, optional=False, tooltip: str=None,):
for x in self._io_types: super().__init__(id, display_name, optional, tooltip)
if not is_class(x): self._io_types = io_types
io_types.append(type(x))
else: @property
io_types.append(x) def io_types(self) -> list[type[InputV3]]:
return io_types '''
Returns list of InputV3 class types permitted.
def get_io_type_V1(self): '''
return ",".join(x.io_type for x in self.io_types) io_types = []
for x in self._io_types:
if not is_class(x):
io_types.append(type(x))
else:
io_types.append(x)
return io_types
def get_io_type_V1(self):
return ",".join(x.io_type for x in self.io_types)
class DynamicInput(InputV3):
class DynamicInput(InputV3, io_type=None):
''' '''
Abstract class for dynamic input registration. Abstract class for dynamic input registration.
''' '''
def __init__(self, io_type: str, id: str, display_name: str=None): def __init__(self, io_type: str, id: str, display_name: str=None):
super().__init__(io_type, id, display_name) super().__init__(io_type, id, display_name)
class DynamicOutput(OutputV3, io_type=None): class DynamicOutput(OutputV3):
''' '''
Abstract class for dynamic output registration. Abstract class for dynamic output registration.
''' '''
def __init__(self, io_type: str, id: str, display_name: str=None): def __init__(self, io_type: str, id: str, display_name: str=None):
super().__init__(io_type, id, display_name) super().__init__(io_type, id, display_name)
class AutoGrowDynamicInput(DynamicInput, io_type="COMFY_MULTIGROW_V3"): # io_type="COMFY_MULTIGROW_V3"
class AutoGrowDynamicInput(DynamicInput):
''' '''
Dynamic Input that adds another template_input each time one is provided. Dynamic Input that adds another template_input each time one is provided.
Additional inputs are forced to have 'InputBehavior.optional'. Additional inputs are forced to have 'optional=True'.
''' '''
def __init__(self, id: str, template_input: InputV3, min: int=1, max: int=None): def __init__(self, id: str, template_input: InputV3, min: int=1, max: int=None):
super().__init__("AutoGrowDynamicInput", id) super().__init__("AutoGrowDynamicInput", id)
@ -542,11 +540,12 @@ class AutoGrowDynamicInput(DynamicInput, io_type="COMFY_MULTIGROW_V3"):
self.min = min self.min = min
self.max = max self.max = max
class ComboDynamicInput(DynamicInput, io_type="COMFY_COMBODYNAMIC_V3"): # io_type="COMFY_COMBODYNAMIC_V3"
class ComboDynamicInput(DynamicInput):
def __init__(self, id: str): def __init__(self, id: str):
pass pass
AutoGrowDynamicInput(id="dynamic", template_input=ImageInput(id="image")) AutoGrowDynamicInput(id="dynamic", template_input=Image.Input(id="image"))
class Hidden: class Hidden:
@ -581,12 +580,10 @@ class Hidden:
api_key_comfy_org=d.get(HiddenEnum.api_key_comfy_org), api_key_comfy_org=d.get(HiddenEnum.api_key_comfy_org),
) )
class HiddenEnum(str, Enum): class HiddenEnum(str, Enum):
''' '''
Enumerator for requesting hidden variables in nodes. Enumerator for requesting hidden variables in nodes.
''' '''
unique_id = "UNIQUE_ID" unique_id = "UNIQUE_ID"
"""UNIQUE_ID is the unique identifier of the node, and matches the id property of the node on the client side. It is commonly used in client-server communications (see messages).""" """UNIQUE_ID is the unique identifier of the node, and matches the id property of the node on the client side. It is commonly used in client-server communications (see messages)."""
prompt = "PROMPT" prompt = "PROMPT"
@ -746,16 +743,6 @@ class Serializer:
pass pass
def prepare_class_clone(c: ComfyNodeV3 | type[ComfyNodeV3]) -> type[ComfyNodeV3]:
"""Creates clone of real node class to prevent monkey-patching."""
c_type: type[ComfyNodeV3] = c if is_class(c) else type(c)
type_clone: type[ComfyNodeV3] = type(f"CLEAN_{c_type.__name__}", c_type.__bases__, {})
# TODO: what parameters should be carried over?
type_clone.SCHEMA = c_type.SCHEMA
# TODO: add anything we would want to expose inside node's execute function
return type_clone
class classproperty(object): class classproperty(object):
def __init__(self, f): def __init__(self, f):
self.f = f self.f = f
@ -763,11 +750,15 @@ class classproperty(object):
return self.f(owner) return self.f(owner)
class ComfyNodeV3(ABC): class ComfyNodeV3:
"""Common base class for all V3 nodes.""" """Common base class for all V3 nodes."""
RELATIVE_PYTHON_MODULE = None RELATIVE_PYTHON_MODULE = None
SCHEMA = None SCHEMA = None
# filled in during execution
state: NodeState = None
hidden: Hidden = None
@classmethod @classmethod
def GET_NODE_INFO_V3(cls) -> dict[str, Any]: def GET_NODE_INFO_V3(cls) -> dict[str, Any]:
@ -795,6 +786,7 @@ class ComfyNodeV3(ABC):
return [] return []
def __init__(self): def __init__(self):
self.local_state: NodeStateLocal = None
self.__class__.VALIDATE_CLASS() self.__class__.VALIDATE_CLASS()
@classmethod @classmethod
@ -811,6 +803,7 @@ class ComfyNodeV3(ABC):
type_clone: type[ComfyNodeV3] = type(f"CLEAN_{c_type.__name__}", c_type.__bases__, {}) type_clone: type[ComfyNodeV3] = type(f"CLEAN_{c_type.__name__}", c_type.__bases__, {})
# TODO: what parameters should be carried over? # TODO: what parameters should be carried over?
type_clone.SCHEMA = c_type.SCHEMA type_clone.SCHEMA = c_type.SCHEMA
type_clone.hidden = Hidden.from_dict(hidden_inputs)
# TODO: add anything we would want to expose inside node's execute function # TODO: add anything we would want to expose inside node's execute function
return type_clone return type_clone
@ -989,14 +982,6 @@ class ComfyNodeV3(ABC):
#-------------------------------------------- #--------------------------------------------
############################################# #############################################
# class ReturnedInputs:
# def __init__(self):
# pass
# class ReturnedOutputs:
# def __init__(self):
# pass
class NodeOutput: class NodeOutput:
''' '''
@ -1007,9 +992,11 @@ class NodeOutput:
self.ui = ui self.ui = ui
self.expand = expand self.expand = expand
self.block_execution = block_execution self.block_execution = block_execution
# self.kwargs = kwargs
@property @property
def result(self): def result(self):
# TODO: use kwargs to refer to outputs by id + organize in proper order
return self.args if len(self.args) > 0 else None return self.args if len(self.args) > 0 else None
@ -1084,6 +1071,11 @@ class UIText(UIOutput):
return {"text": (self.value,)} return {"text": (self.value,)}
def create_image_preview(image: Image.Type) -> UIImages:
# TODO: finish, right now is just Cursor's hallucination
return UIImages([SavedResult("preview.png", "comfy_org", FolderType.output)])
class TestNode(ComfyNodeV3): class TestNode(ComfyNodeV3):
@classmethod @classmethod
def DEFINE_SCHEMA(cls): def DEFINE_SCHEMA(cls):
@ -1091,11 +1083,11 @@ class TestNode(ComfyNodeV3):
node_id="TestNode_v3", node_id="TestNode_v3",
display_name="Test Node (V3)", display_name="Test Node (V3)",
category="v3_test", category="v3_test",
inputs=[IntegerInput("my_int"), inputs=[Int.Input("my_int"),
#AutoGrowDynamicInput("growing", ImageInput), #AutoGrowDynamicInput("growing", Image.Input),
MaskInput("thing"), Mask.Input("thing"),
], ],
outputs=[ImageOutput("image_output")], outputs=[Image.Output("image_output")],
hidden=[HiddenEnum.api_key_comfy_org, HiddenEnum.auth_token_comfy_org, HiddenEnum.unique_id] hidden=[HiddenEnum.api_key_comfy_org, HiddenEnum.auth_token_comfy_org, HiddenEnum.unique_id]
) )
@ -1103,26 +1095,25 @@ class TestNode(ComfyNodeV3):
def execute(cls, **kwargs): def execute(cls, **kwargs):
pass pass
if __name__ == "__main__": if __name__ == "__main__":
print("hello there") print("hello there")
inputs: list[InputV3] = [ inputs: list[InputV3] = [
IntegerInput("tessfes", widgetType=IO.STRING), Int.Input("tessfes", widgetType=IO.STRING),
IntegerInput("my_int"), Int.Input("my_int"),
CustomInput("xyz", "XYZ"), Custom("XYZ").Input("xyz"),
CustomInput("model1", "MODEL_M"), Custom("MODEL_M").Input("model1"),
ImageInput("my_image"), Image.Input("my_image"),
FloatInput("my_float"), Float.Input("my_float"),
MultitypedInput("my_inputs", [StringInput, CustomType("MODEL_M"), CustomType("XYZ")]), MultiType.Input("my_inputs", [String, Custom("MODEL_M"), Custom("XYZ")]),
] ]
Custom("XYZ").Input()
outputs: list[OutputV3] = [ outputs: list[OutputV3] = [
ImageOutput("image"), Image.Output("image"),
CustomOutput("xyz", "XYZ") Custom("XYZ").Output("xyz"),
] ]
for c in inputs: for c in inputs:
if isinstance(c, MultitypedInput): if isinstance(c, MultiType):
print(f"{c}, {type(c)}, {type(c).io_type}, {c.id}, {[x.io_type for x in c.io_types]}") print(f"{c}, {type(c)}, {type(c).io_type}, {c.id}, {[x.io_type for x in c.io_types]}")
print(c.get_io_type_V1()) print(c.get_io_type_V1())
else: else:

File diff suppressed because it is too large Load Diff

View File

@ -1,94 +0,0 @@
import torch
from comfy_api.v3_01 import io
import logging
@io.comfytype(io_type="XYZ")
class XYZ:
Type = tuple[int,str]
class Input(io.InputV3):
...
class Output(io.OutputV3):
...
class MyState(io.NodeState):
my_str: str
my_int: int
class V3TestNode(io.ComfyNodeV3):
state: MyState
def __init__(self):
super().__init__()
self.hahajkunless = ";)"
@classmethod
def DEFINE_SCHEMA(cls):
return io.SchemaV3(
node_id="V3_01_TestNode1",
display_name="V3_01 Test Node",
description="This is a funky V3_01 node test.",
category="v3 nodes",
inputs=[
io.Image.Input("image", display_name="new_image"),
XYZ.Input("xyz", optional=True),
io.Custom("JKL").Input("jkl", optional=True),
#JKL.Input("jkl", optional=True),
#CustomInput("xyz", "XYZ", optional=True),
io.Mask.Input("mask", optional=True),
io.Int.Input("some_int", display_name="new_name", min=0, max=127, default=42,
tooltip="My tooltip 😎", display_mode=io.NumberDisplay.slider),
io.Combo.Input("combo", options=["a", "b", "c"], tooltip="This is a combo input"),
io.MultiCombo.Input("combo2", options=["a","b","c"]),
# ComboInput("combo", image_upload=True, image_folder=FolderType.output,
# remote=RemoteOptions(
# route="/internal/files/output",
# refresh_button=True,
# ),
# tooltip="This is a combo input"),
# IntegerInput("some_int", display_name="new_name", min=0, tooltip="My tooltip 😎", display=NumberDisplay.slider, ),
# ComboDynamicInput("mask", behavior=InputBehavior.optional),
# IntegerInput("some_int", display_name="new_name", min=0, tooltip="My tooltip 😎", display=NumberDisplay.slider,
# dependent_inputs=[ComboDynamicInput("mask", behavior=InputBehavior.optional)],
# dependent_values=[lambda my_value: IO.STRING if my_value < 5 else IO.NUMBER],
# ),
# ["option1", "option2". "option3"]
# ComboDynamicInput["sdfgjhl", [ComboDynamicOptions("option1", [IntegerInput("some_int", display_name="new_name", min=0, tooltip="My tooltip 😎", display=NumberDisplay.slider, ImageInput(), MaskInput(), String()]),
# CombyDynamicOptons("option2", [])
# ]]
],
outputs=[
io.Int.Output("int_output"),
io.Image.Output("img_output", display_name="img🖼", tooltip="This is an image"),
],
hidden=[
],
is_output_node=True,
)
@classmethod
def execute(cls, image: io.Image.Type, some_int: int, combo: io.Combo.Type, combo2: io.MultiCombo.Type, xyz: XYZ.Type=None, mask: io.Mask.Type=None, **kwargs):
zzz = cls.hidden.prompt
cls.state.my_str = "LOLJK"
expected_int = 123
cls.state.my_int = expected_int
if cls.state.my_int is None:
cls.state.my_int = expected_int
else:
if cls.state.my_int != expected_int:
raise Exception(f"Explicit state object did not maintain expected value: {cls.state.my_int} != {expected_int}")
#some_int
if hasattr(cls, "hahajkunless"):
raise Exception("The 'cls' variable leaked instance state between runs!")
if hasattr(cls, "doohickey"):
raise Exception("The 'cls' variable leaked state on class properties between runs!")
cls.doohickey = "LOLJK"
return io.NodeOutput(some_int, image)
NODES_LIST: list[io.ComfyNodeV3] = [
V3TestNode,
]

View File

@ -1,40 +1,47 @@
import torch import torch
from comfy_api.v3.io import ( from comfy_api.v3 import io
ComfyNodeV3, SchemaV3, NumberDisplay,
IntegerInput, MaskInput, ImageInput, ComboInput, CustomInput, StringInput, CustomType,
IntegerOutput, ImageOutput, MultitypedInput, InputV3, OutputV3,
NodeOutput
)
import logging import logging
class XYZInput(InputV3, io_type="XYZ"): @io.comfytype(io_type="XYZ")
class XYZ:
Type = tuple[int,str] Type = tuple[int,str]
class Input(io.InputV3):
...
class Output(io.OutputV3):
...
class XYZOutput(OutputV3, io_type="XYZ"): class MyState(io.NodeState):
... my_str: str
my_int: int
class V3TestNode(ComfyNodeV3): class V3TestNode(io.ComfyNodeV3):
state: MyState
def __init__(self): def __init__(self):
super().__init__()
self.hahajkunless = ";)" self.hahajkunless = ";)"
@classmethod @classmethod
def DEFINE_SCHEMA(cls): def DEFINE_SCHEMA(cls):
return SchemaV3( return io.SchemaV3(
node_id="V3TestNode1", node_id="V3_01_TestNode1",
display_name="V3 Test Node", display_name="V3 Test Node",
description="This is a funky V3 node test.", description="This is a funky V3 node test.",
category="v3 nodes", category="v3 nodes",
inputs=[ inputs=[
ImageInput("image", display_name="new_image"), io.Image.Input("image", display_name="new_image"),
XYZInput("xyz", optional=True), XYZ.Input("xyz", optional=True),
io.Custom("JKL").Input("jkl", optional=True),
#JKL.Input("jkl", optional=True),
#CustomInput("xyz", "XYZ", optional=True), #CustomInput("xyz", "XYZ", optional=True),
MaskInput("mask", optional=True), io.Mask.Input("mask", optional=True),
IntegerInput("some_int", display_name="new_name", min=0, max=127, default=42, io.Int.Input("some_int", display_name="new_name", min=0, max=127, default=42,
tooltip="My tooltip 😎", display_mode=NumberDisplay.slider), tooltip="My tooltip 😎", display_mode=io.NumberDisplay.slider),
ComboInput("combo", options=["a", "b", "c"], tooltip="This is a combo input"), io.Combo.Input("combo", options=["a", "b", "c"], tooltip="This is a combo input"),
io.MultiCombo.Input("combo2", options=["a","b","c"]),
# ComboInput("combo", image_upload=True, image_folder=FolderType.output, # ComboInput("combo", image_upload=True, image_folder=FolderType.output,
# remote=RemoteOptions( # remote=RemoteOptions(
# route="/internal/files/output", # route="/internal/files/output",
@ -53,8 +60,8 @@ class V3TestNode(ComfyNodeV3):
# ]] # ]]
], ],
outputs=[ outputs=[
IntegerOutput("int_output"), io.Int.Output("int_output"),
ImageOutput("img_output", display_name="img🖼", tooltip="This is an image"), io.Image.Output("img_output", display_name="img🖼", tooltip="This is an image"),
], ],
hidden=[ hidden=[
@ -63,15 +70,25 @@ class V3TestNode(ComfyNodeV3):
) )
@classmethod @classmethod
def execute(cls, image: ImageInput.Type, some_int: IntegerInput.Type, combo: ComboInput.Type, xyz: XYZInput.Type=None, mask: MaskInput.Type=None): def execute(cls, image: io.Image.Type, some_int: int, combo: io.Combo.Type, combo2: io.MultiCombo.Type, xyz: XYZ.Type=None, mask: io.Mask.Type=None, **kwargs):
zzz = cls.hidden.prompt
cls.state.my_str = "LOLJK"
expected_int = 123
cls.state.my_int = expected_int
if cls.state.my_int is None:
cls.state.my_int = expected_int
else:
if cls.state.my_int != expected_int:
raise Exception(f"Explicit state object did not maintain expected value: {cls.state.my_int} != {expected_int}")
#some_int
if hasattr(cls, "hahajkunless"): if hasattr(cls, "hahajkunless"):
raise Exception("The 'cls' variable leaked instance state between runs!") raise Exception("The 'cls' variable leaked instance state between runs!")
if hasattr(cls, "doohickey"): if hasattr(cls, "doohickey"):
raise Exception("The 'cls' variable leaked state on class properties between runs!") raise Exception("The 'cls' variable leaked state on class properties between runs!")
cls.doohickey = "LOLJK" cls.doohickey = "LOLJK"
return NodeOutput(some_int, image) return io.NodeOutput(some_int, image)
NODES_LIST: list[ComfyNodeV3] = [ NODES_LIST: list[io.ComfyNodeV3] = [
V3TestNode, V3TestNode,
] ]

View File

@ -28,8 +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, HiddenEnum from comfy_api.v3.io import NodeOutput, ComfyNodeV3, HiddenEnum, NodeStateLocal
from comfy_api.v3_01.io import NodeStateLocal
class ExecutionResult(Enum): class ExecutionResult(Enum):

View File

@ -2293,7 +2293,6 @@ def init_builtin_extra_nodes():
"nodes_string.py", "nodes_string.py",
"nodes_camera_trajectory.py", "nodes_camera_trajectory.py",
"nodes_v3_test.py", "nodes_v3_test.py",
"nodes_v3_01_test.py",
"nodes_v1_test.py", "nodes_v1_test.py",
] ]