diff --git a/comfy_api/internal/__init__.py b/comfy_api/internal/__init__.py index 939d09b8d..7321b7845 100644 --- a/comfy_api/internal/__init__.py +++ b/comfy_api/internal/__init__.py @@ -36,6 +36,13 @@ class _ComfyNodeInternal: ... +class _NodeOutputInternal: + """Class that all V3-based APIs inherit from for NodeOutput. + + This is intended to only be referenced within execution.py, as it has to handle all V3 APIs going forward.""" + ... + + def as_pruned_dict(dataclass_obj): '''Return dict of dataclass object with pruned None values.''' return prune_dict(asdict(dataclass_obj)) diff --git a/comfy_api/v3/__init__.py b/comfy_api/v3/__init__.py index e69de29bb..b3a62e65a 100644 --- a/comfy_api/v3/__init__.py +++ b/comfy_api/v3/__init__.py @@ -0,0 +1,9 @@ +from comfy_api.v3._io import _IO +from comfy_api.v3._ui import _UI +from comfy_api.v3._resources import _RESOURCES + +io = _IO +ui = _UI +resources = _RESOURCES + +__all__ = ["io", "ui", "resources"] diff --git a/comfy_api/v3/io.py b/comfy_api/v3/_io.py similarity index 94% rename from comfy_api/v3/io.py rename to comfy_api/v3/_io.py index d1fd22deb..f9c7a8cc1 100644 --- a/comfy_api/v3/io.py +++ b/comfy_api/v3/_io.py @@ -22,14 +22,13 @@ from comfy.samplers import CFGGuider, Sampler from comfy.sd import CLIP, VAE from comfy.sd import StyleModel as StyleModel_ from comfy_api.input import VideoInput -from comfy_api.internal import (_ComfyNodeInternal, classproperty, copy_class, first_real_override, is_class, +from comfy_api.internal import (_ComfyNodeInternal, _NodeOutputInternal, classproperty, copy_class, first_real_override, is_class, prune_dict, shallow_clone_class) -from comfy_api.v3.resources import Resources, ResourcesLocal +from comfy_api.v3._resources import Resources, ResourcesLocal from comfy_execution.graph import ExecutionBlocker # from comfy_extras.nodes_images import SVG as SVG_ # NOTE: needs to be moved before can be imported due to circular reference - class FolderType(str, Enum): input = "input" output = "output" @@ -157,7 +156,7 @@ class _IO_V3: def Type(self): return self.Parent.Type -class InputV3(_IO_V3): +class Input(_IO_V3): ''' Base class for a V3 Input. ''' @@ -181,7 +180,7 @@ class InputV3(_IO_V3): def get_io_type(self): return _StringIOType(self.io_type) -class WidgetInputV3(InputV3): +class WidgetInput(Input): ''' Base class for a V3 Input with widget. ''' @@ -206,7 +205,7 @@ class WidgetInputV3(InputV3): return self.widget_type if self.widget_type is not None else super().get_io_type() -class OutputV3(_IO_V3): +class Output(_IO_V3): def __init__(self, id: str=None, display_name: str=None, tooltip: str=None, is_output_list=False): self.id = id @@ -227,12 +226,12 @@ class OutputV3(_IO_V3): class ComfyTypeI(_ComfyType): '''ComfyType subclass that only has a default Input class - intended for types that only have Inputs.''' - class Input(InputV3): + class Input(Input): ... class ComfyTypeIO(ComfyTypeI): '''ComfyType subclass that has default Input and Output classes; useful for types with both Inputs and Outputs.''' - class Output(OutputV3): + class Output(Output): ... @@ -240,7 +239,7 @@ class ComfyTypeIO(ComfyTypeI): class Boolean(ComfyTypeIO): Type = bool - class Input(WidgetInputV3): + class Input(WidgetInput): '''Boolean input.''' 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, @@ -260,7 +259,7 @@ class Boolean(ComfyTypeIO): class Int(ComfyTypeIO): Type = int - class Input(WidgetInputV3): + class Input(WidgetInput): '''Integer input.''' 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, @@ -286,7 +285,7 @@ class Int(ComfyTypeIO): class Float(ComfyTypeIO): Type = float - class Input(WidgetInputV3): + class Input(WidgetInput): '''Float input.''' 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, @@ -312,7 +311,7 @@ class Float(ComfyTypeIO): class String(ComfyTypeIO): Type = str - class Input(WidgetInputV3): + class Input(WidgetInput): '''String input.''' def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None, multiline=False, placeholder: str=None, default: str=None, dynamic_prompts: bool=None, @@ -333,7 +332,7 @@ class String(ComfyTypeIO): @comfytype(io_type="COMBO") class Combo(ComfyTypeI): Type = str - class Input(WidgetInputV3): + class Input(WidgetInput): """Combo input (dropdown).""" Type = str def __init__(self, id: str, options: list[str]=None, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None, @@ -398,7 +397,7 @@ class WanCameraEmbedding(ComfyTypeIO): class Webcam(ComfyTypeIO): Type = str - class Input(WidgetInputV3): + class Input(WidgetInput): """Webcam input.""" Type = str def __init__( @@ -715,14 +714,14 @@ class AnyType(ComfyTypeIO): @comfytype(io_type="COMFY_MULTITYPED_V3") class MultiType: Type = Any - class Input(InputV3): + class Input(Input): ''' Input that permits more than one input type; if `id` is an instance of `ComfyType.Input`, then that input will be used to create a widget (if applicable) with overridden values. ''' - def __init__(self, id: str | InputV3, types: list[type[_ComfyType] | _ComfyType], display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None, extra_dict=None): + def __init__(self, id: str | Input, types: list[type[_ComfyType] | _ComfyType], display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None, extra_dict=None): # if id is an Input, then use that Input with overridden values self.input_override = None - if isinstance(id, InputV3): + if isinstance(id, Input): self.input_override = copy.copy(id) optional = id.optional if id.optional is True else optional tooltip = id.tooltip if id.tooltip is not None else tooltip @@ -730,13 +729,13 @@ class MultiType: lazy = id.lazy if id.lazy is not None else lazy id = id.id # if is a widget input, make sure widget_type is set appropriately - if isinstance(self.input_override, WidgetInputV3): + if isinstance(self.input_override, WidgetInput): self.input_override.widget_type = self.input_override.get_io_type() super().__init__(id, display_name, optional, tooltip, lazy, extra_dict) self._io_types = types @property - def io_types(self) -> list[type[InputV3]]: + def io_types(self) -> list[type[Input]]: ''' Returns list of InputV3 class types permitted. ''' @@ -761,15 +760,15 @@ class MultiType: else: return super().as_dict() -class DynamicInput(InputV3, ABC): +class DynamicInput(Input, ABC): ''' Abstract class for dynamic input registration. ''' @abstractmethod - def get_dynamic(self) -> list[InputV3]: + def get_dynamic(self) -> list[Input]: ... -class DynamicOutput(OutputV3, ABC): +class DynamicOutput(Output, ABC): ''' Abstract class for dynamic output registration. ''' @@ -778,7 +777,7 @@ class DynamicOutput(OutputV3, ABC): super().__init__(id, display_name, tooltip, is_output_list) @abstractmethod - def get_dynamic(self) -> list[OutputV3]: + def get_dynamic(self) -> list[Output]: ... @@ -786,7 +785,7 @@ class DynamicOutput(OutputV3, ABC): class AutogrowDynamic(ComfyTypeI): Type = list[Any] class Input(DynamicInput): - def __init__(self, id: str, template_input: InputV3, min: int=1, max: int=None, + def __init__(self, id: str, template_input: Input, min: int=1, max: int=None, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None, extra_dict=None): super().__init__(id, display_name, optional, tooltip, lazy, extra_dict) self.template_input = template_input @@ -797,7 +796,7 @@ class AutogrowDynamic(ComfyTypeI): self.min = min self.max = max - def get_dynamic(self) -> list[InputV3]: + def get_dynamic(self) -> list[Input]: curr_count = 1 new_inputs = [] for i in range(self.min): @@ -806,7 +805,7 @@ class AutogrowDynamic(ComfyTypeI): if new_input.display_name is not None: new_input.display_name = f"{new_input.display_name}{curr_count}" new_input.optional = self.optional or new_input.optional - if isinstance(self.template_input, WidgetInputV3): + if isinstance(self.template_input, WidgetInput): new_input.force_input = True new_inputs.append(new_input) curr_count += 1 @@ -817,17 +816,17 @@ class AutogrowDynamic(ComfyTypeI): if new_input.display_name is not None: new_input.display_name = f"{new_input.display_name}{curr_count}" new_input.optional = True - if isinstance(self.template_input, WidgetInputV3): + if isinstance(self.template_input, WidgetInput): new_input.force_input = True new_inputs.append(new_input) curr_count += 1 return new_inputs -# io_type="COMFY_COMBODYNAMIC_V3" -class ComboDynamicInput(DynamicInput): - def __init__(self, id: str): - pass - +@comfytype(io_type="COMFY_COMBODYNAMIC_V3") +class ComboDynamic(ComfyTypeI): + class Input(DynamicInput): + def __init__(self, id: str): + pass @comfytype(io_type="COMFY_MATCHTYPE_V3") class MatchType(ComfyTypeIO): @@ -848,7 +847,7 @@ class MatchType(ComfyTypeIO): super().__init__(id, display_name, optional, tooltip, lazy, extra_dict) self.template = template - def get_dynamic(self) -> list[InputV3]: + def get_dynamic(self) -> list[Input]: return [self] def as_dict(self): @@ -862,7 +861,7 @@ class MatchType(ComfyTypeIO): super().__init__(id, display_name, tooltip, is_output_list) self.template = template - def get_dynamic(self) -> list[OutputV3]: + def get_dynamic(self) -> list[Output]: return [self] def as_dict(self): @@ -966,8 +965,8 @@ class Schema: """Display name of node.""" category: str = "sd" """The category of the node, as per the "Add Node" menu.""" - inputs: list[InputV3]=None - outputs: list[OutputV3]=None + inputs: list[Input]=None + outputs: list[Output]=None hidden: list[Hidden]=None description: str="" """Node description, shown as a tooltip when hovering over the node.""" @@ -1129,14 +1128,14 @@ class Schema: return info -def add_to_dict_v1(i: InputV3, input: dict): +def add_to_dict_v1(i: Input, input: dict): key = "optional" if i.optional else "required" as_dict = i.as_dict() # for v1, we don't want to include the optional key as_dict.pop("optional", None) input.setdefault(key, {})[i.id] = (i.get_io_type(), as_dict) -def add_to_dict_v3(io: InputV3 | OutputV3, d: dict): +def add_to_dict_v3(io: Input | Output, d: dict): d[io.id] = (io.get_io_type(), io.as_dict()) @@ -1487,7 +1486,7 @@ class ComfyNode(_ComfyNodeBaseInternal): return ComfyNode -class NodeOutput: +class NodeOutput(_NodeOutputInternal): ''' Standardized output of a node; can pass in any number of args and/or a UIOutput into 'ui' kwarg. ''' @@ -1527,3 +1526,78 @@ class _UIOutput(ABC): @abstractmethod def as_dict(self) -> dict: ... + + +class _IO: + FolderType = FolderType + UploadType = UploadType + RemoteOptions = RemoteOptions + NumberDisplay = NumberDisplay + + comfytype = staticmethod(comfytype) + Custom = staticmethod(Custom) + Input = Input + WidgetInput = WidgetInput + Output = Output + ComfyTypeI = ComfyTypeI + ComfyTypeIO = ComfyTypeIO + #--------------------------------- + # Supported Types + Boolean = Boolean + Int = Int + Float = Float + String = String + Combo = Combo + MultiCombo = MultiCombo + Image = Image + WanCameraEmbedding = WanCameraEmbedding + Webcam = Webcam + Mask = Mask + Latent = Latent + Conditioning = Conditioning + Sampler = Sampler + Sigmas = Sigmas + Noise = Noise + Guider = Guider + Clip = Clip + ControlNet = ControlNet + Vae = Vae + Model = Model + ClipVision = ClipVision + ClipVisionOutput = ClipVisionOutput + StyleModel = StyleModel + Gligen = Gligen + UpscaleModel = UpscaleModel + Audio = Audio + Video = Video + SVG = SVG + LoraModel = LoraModel + LossMap = LossMap + Voxel = Voxel + Mesh = Mesh + Hooks = Hooks + HookKeyframes = HookKeyframes + TimestepsRange = TimestepsRange + LatentOperation = LatentOperation + FlowControl = FlowControl + Accumulation = Accumulation + Load3DCamera = Load3DCamera + Load3D = Load3D + Load3DAnimation = Load3DAnimation + Photomaker = Photomaker + Point = Point + FaceAnalysis = FaceAnalysis + BBOX = BBOX + SEGS = SEGS + AnyType = AnyType + MultiType = MultiType + #--------------------------------- + HiddenHolder = HiddenHolder + Hidden = Hidden + NodeInfoV1 = NodeInfoV1 + NodeInfoV3 = NodeInfoV3 + Schema = Schema + ComfyNode = ComfyNode + NodeOutput = NodeOutput + add_to_dict_v1 = staticmethod(add_to_dict_v1) + add_to_dict_v3 = staticmethod(add_to_dict_v3) diff --git a/comfy_api/v3/resources.py b/comfy_api/v3/_resources.py similarity index 93% rename from comfy_api/v3/resources.py rename to comfy_api/v3/_resources.py index 12c751275..a6bdda972 100644 --- a/comfy_api/v3/resources.py +++ b/comfy_api/v3/_resources.py @@ -63,3 +63,10 @@ class ResourcesLocal(Resources): if default is not ...: return default raise Exception(f"Unsupported resource key type: {type(key)}") + + +class _RESOURCES: + ResourceKey = ResourceKey + TorchDictFolderFilename = TorchDictFolderFilename + Resources = Resources + ResourcesLocal = ResourcesLocal diff --git a/comfy_api/v3/ui.py b/comfy_api/v3/_ui.py similarity index 97% rename from comfy_api/v3/ui.py rename to comfy_api/v3/_ui.py index 4094812e0..fc959f6b4 100644 --- a/comfy_api/v3/ui.py +++ b/comfy_api/v3/_ui.py @@ -17,7 +17,7 @@ import folder_paths # used for image preview from comfy.cli_args import args -from comfy_api.v3.io import ComfyNode, FolderType, Image, _UIOutput +from comfy_api.v3._io import ComfyNode, FolderType, Image, _UIOutput class SavedResult(dict): @@ -489,3 +489,17 @@ class PreviewText(_UIOutput): def as_dict(self): return {"text": (self.value,)} + + +class _UI: + SavedResult = SavedResult + SavedImages = SavedImages + SavedAudios = SavedAudios + ImageSaveHelper = ImageSaveHelper + AudioSaveHelper = AudioSaveHelper + PreviewImage = PreviewImage + PreviewMask = PreviewMask + PreviewAudio = PreviewAudio + PreviewVideo = PreviewVideo + PreviewUI3D = PreviewUI3D + PreviewText = PreviewText diff --git a/comfy_extras/nodes_v3_test.py b/comfy_extras/nodes_v3_test.py index b8e5e2ae0..d78fa9d4b 100644 --- a/comfy_extras/nodes_v3_test.py +++ b/comfy_extras/nodes_v3_test.py @@ -1,13 +1,12 @@ import torch import time -from comfy_api.v3 import io, ui, resources +from comfy_api.v3 import io, ui, resources, _io import logging # noqa import folder_paths import comfy.utils import comfy.sd import asyncio - @io.comfytype(io_type="XYZ") class XYZ(io.ComfyTypeIO): Type = tuple[int,str] @@ -144,8 +143,8 @@ class NInputsTest(io.ComfyNode): node_id="V3_NInputsTest", display_name="V3 N Inputs Test", inputs=[ - io.AutogrowDynamic.Input("nmock", template_input=io.Image.Input("image"), min=1, max=3), - io.AutogrowDynamic.Input("nmock2", template_input=io.Int.Input("int"), optional=True, min=1, max=4), + _io.AutogrowDynamic.Input("nmock", template_input=io.Image.Input("image"), min=1, max=3), + _io.AutogrowDynamic.Input("nmock2", template_input=io.Int.Input("int"), optional=True, min=1, max=4), ], outputs=[ io.Image.Output(), diff --git a/execution.py b/execution.py index 49536bda4..dae0d0390 100644 --- a/execution.py +++ b/execution.py @@ -32,8 +32,8 @@ from comfy_execution.graph_utils import GraphBuilder, is_link from comfy_execution.validation import validate_node_input from comfy_execution.progress import get_progress_state, reset_progress_state, add_progress_handler, WebUIProgressHandler from comfy_execution.utils import CurrentNodeContext -from comfy_api.internal import _ComfyNodeInternal, first_real_override, is_class, make_locked_method_func -from comfy_api.v3 import io +from comfy_api.internal import _ComfyNodeInternal, _NodeOutputInternal, first_real_override, is_class, make_locked_method_func +from comfy_api.v3 import io, resources class ExecutionResult(Enum): @@ -256,26 +256,11 @@ async def _async_map_node_over_list(prompt_id, unique_id, obj, input_data_all, f type_obj = type(obj) type_obj.VALIDATE_CLASS() class_clone = type_obj.PREPARE_CLASS_CLONE(hidden_inputs) - # NOTE: this is a mock of state management; for local, just stores NodeStateLocal on node instance - if hasattr(obj, "local_state"): - if obj.local_state is None: - obj.local_state = io.NodeStateLocal(class_clone.hidden.unique_id) - class_clone.state = obj.local_state # NOTE: this is a mock of resource management; for local, just stores ResourcesLocal on node instance if hasattr(obj, "local_resources"): if obj.local_resources is None: - obj.local_resources = io.ResourcesLocal() + obj.local_resources = resources.ResourcesLocal() class_clone.resources = obj.local_resources - # TODO: delete this when done testing mocking dynamic inputs - for si in obj.SCHEMA.inputs: - if isinstance(si, io.AutogrowDynamic.Input): - add_key = si.id - dynamic_list = [] - real_inputs = {k: v for k, v in inputs.items()} - for d in si.get_dynamic(): - dynamic_list.append(real_inputs.pop(d.id, None)) - dynamic_list = [x for x in dynamic_list if x is not None] - inputs = {**real_inputs, add_key: dynamic_list} f = make_locked_method_func(type_obj, func, class_clone) # V1 else: @@ -363,7 +348,7 @@ def get_output_from_returns(return_values, obj): result = tuple([result] * len(obj.RETURN_TYPES)) results.append(result) subgraph_results.append((None, result)) - elif isinstance(r, io.NodeOutput): + elif isinstance(r, _NodeOutputInternal): # V3 if r.ui is not None: if isinstance(r.ui, dict):