diff --git a/comfy_api/v3/io.py b/comfy_api/v3/io.py index 0c79111d..7e13f956 100644 --- a/comfy_api/v3/io.py +++ b/comfy_api/v3/io.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Union +from typing import Union, Any from enum import Enum from abc import ABC, abstractmethod @@ -17,7 +17,15 @@ def is_class(obj): return isinstance(obj, type) +class NumberDisplay(str, Enum): + number = "number" + slider = "slider" + + class IO_V3: + ''' + Base class for V3 Inputs and Outputs. + ''' def __init__(self): pass @@ -26,18 +34,34 @@ class IO_V3: super().__init_subclass__(**kwargs) class InputV3(IO_V3, io_type=None): - def __init__(self, id: str, display_name: str=None, behavior=InputBehavior.required, tooltip: str=None): + ''' + Base class for a V3 Input. + ''' + def __init__(self, id: str, display_name: str=None, behavior=InputBehavior.required, tooltip: str=None, lazy: bool=None): super().__init__() self.id = id self.display_name = display_name self.behavior = behavior self.tooltip = tooltip + self.lazy = lazy + +class WidgetInputV3(InputV3, io_type=None): + ''' + Base class for a V3 Input with widget. + ''' + def __init__(self, id: str, display_name: str=None, behavior=InputBehavior.required, tooltip: str=None, lazy: bool=None, + default: Any=None, + socketless: bool=None, widgetType: str=None): + super().__init__(id, display_name, behavior, tooltip, lazy) + self.default = default + self.socketless = socketless + self.widgetType = widgetType def CustomType(io_type: str) -> type[IO_V3]: name = f"{io_type}_IO_V3" return type(name, (IO_V3,), {}, io_type=io_type) -def CustomInput(io_type: str, id: str, display_name: str=None, behavior=InputBehavior.required, tooltip: str=None) -> InputV3: +def CustomInput(id: str, io_type: str, display_name: str=None, behavior=InputBehavior.required, tooltip: str=None, lazy: bool=None) -> InputV3: ''' Defines input for 'io_type'. Can be used to stand in for non-core types. ''' @@ -46,10 +70,11 @@ def CustomInput(io_type: str, id: str, display_name: str=None, behavior=InputBeh "display_name": display_name, "behavior": behavior, "tooltip": tooltip, + "lazy": lazy, } return type(f"{io_type}Input", (InputV3,), {}, io_type=io_type)(**input_kwargs) -def CustomOutput(io_type: str, id: str, display_name: str=None, tooltip: str=None) -> OutputV3: +def CustomOutput(id: str, io_type: str, display_name: str=None, tooltip: str=None) -> OutputV3: ''' Defines output for 'io_type'. Can be used to stand in for non-core types. ''' @@ -61,38 +86,82 @@ def CustomOutput(io_type: str, id: str, display_name: str=None, tooltip: str=Non return type(f"{io_type}Output", (OutputV3,), {}, io_type=io_type)(**input_kwargs) -class IntegerInput(InputV3, io_type="INT"): +class BooleanInput(WidgetInputV3, io_type="BOOLEAN"): + ''' + Boolean input. + ''' + def __init__(self, id: str, display_name: str=None, behavior=InputBehavior.required, 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, behavior, tooltip, lazy, default, socketless, widgetType) + self.label_on = label_on + self.label_off = label_off + self.default: bool + +class IntegerInput(WidgetInputV3, io_type="INT"): ''' Integer input. ''' - def __init__(self, id: str, display_name: str=None, behavior=InputBehavior.required, tooltip: str=None, - min: int=None, max: int=None, step: int=None): - super().__init__(id, display_name, behavior, tooltip) + def __init__(self, id: str, display_name: str=None, behavior=InputBehavior.required, 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, behavior, tooltip, lazy, default, socketless, widgetType) self.min = min self.max = max self.step = step - self.tooltip = tooltip + self.control_after_generate = control_after_generate + self.display_mode = display_mode + self.default: int -class FloatInput(IntegerInput, io_type="FLOAT"): +class FloatInput(WidgetInputV3, io_type="FLOAT"): ''' Float input. ''' - pass + def __init__(self, id: str, display_name: str=None, behavior=InputBehavior.required, 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, behavior, 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 -class StringInput(InputV3, io_type="STRING"): +class StringInput(WidgetInputV3, io_type="STRING"): ''' String input. ''' - def __init__(self, id: str, display_name: str=None, behavior=InputBehavior.required, tooltip: str=None, - multiline=False): - super().__init__(id, display_name, behavior, tooltip) + def __init__(self, id: str, display_name: str=None, behavior=InputBehavior.required, 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, behavior, tooltip, lazy, default, socketless, widgetType) self.multiline = multiline + self.placeholder = placeholder + self.default: str -class ComboInput(InputV3, io_type="COMBO"): +class ComboInput(WidgetInputV3, io_type="COMBO"): '''Combo input (dropdown).''' - def __init__(self, id: str, combo_list: list[str], display_name: str=None, behavior=InputBehavior.required, tooltip: str=None): - super().__init__(id, display_name, behavior, tooltip) - self.combo_list = combo_list + def __init__(self, id: str, options: list[str], display_name: str=None, behavior=InputBehavior.required, tooltip: str=None, lazy: bool=None, + default: str=None, control_after_generate: bool=None, + socketless: bool=None, widgetType: str=None): + super().__init__(id, display_name, behavior, tooltip, lazy, default, socketless, widgetType) + self.multiselect = False + self.options = options + self.control_after_generate = control_after_generate + self.default: str + +class MultiselectComboWidget(ComboInput, io_type="COMBO"): + '''Multiselect Combo input (dropdown for selecting potentially more than one value).''' + def __init__(self, id: str, options: list[str], display_name: str=None, behavior=InputBehavior.required, tooltip: str=None, lazy: bool=None, + default: list[str]=None, placeholder: str=None, chip: bool=None, control_after_generate: bool=None, + socketless: bool=None, widgetType: str=None): + super().__init__(id, options, display_name, behavior, tooltip, lazy, default, control_after_generate, socketless, widgetType) + self.multiselect = True + self.placeholder = placeholder + self.chip = chip + self.default: list[str] class ImageInput(InputV3, io_type="IMAGE"): ''' @@ -119,7 +188,7 @@ class MultitypedInput(InputV3, io_type="COMFY_MULTITYPED_V3"): ''' Input that permits more than one input type. ''' - def __init__(self, id: str, io_types: list[Union[type[IO_V3], InputV3]], display_name: str=None, behavior=InputBehavior.required, tooltip: str=None,): + def __init__(self, id: str, io_types: list[Union[type[IO_V3], InputV3, str]], display_name: str=None, behavior=InputBehavior.required, tooltip: str=None,): super().__init__(id, display_name, behavior, tooltip) self._io_types = io_types @@ -138,39 +207,36 @@ class MultitypedInput(InputV3, io_type="COMFY_MULTITYPED_V3"): class OutputV3: - def __init__(self, id: str, display_name: str, tooltip: str=None): + def __init__(self, id: str, display_name: str=None, tooltip: str=None, + is_output_list=False): self.id = id self.display_name = display_name self.tooltip = tooltip + self.is_output_list = is_output_list def __init_subclass__(cls, io_type, **kwargs): cls.io_type = io_type super().__init_subclass__(**kwargs) class IntegerOutput(OutputV3, io_type="INT"): - def __init__(self, id: str, display_name: str=None, tooltip: str=None): - super().__init__(id, display_name, tooltip) + pass class FloatOutput(OutputV3, io_type="FLOAT"): pass class StringOutput(OutputV3, io_type="STRING"): - def __init__(self, id: str, display_name: str=None, tooltip: str=None): - super().__init__(id, display_name, tooltip) - - + pass + # def __init__(self, id: str, display_name: str=None, tooltip: str=None): + # super().__init__(id, display_name, tooltip) class ImageOutput(OutputV3, io_type="IMAGE"): - def __init__(self, id: str, display_name: str=None, tooltip: str=None): - super().__init__(id, display_name, tooltip) + pass class MaskOutput(OutputV3, io_type="MASK"): - def __init__(self, id: str, display_name: str=None, tooltip: str=None): - super().__init__(id, display_name, tooltip) + pass class LatentOutput(OutputV3, io_type="LATENT"): - def __init__(self, id: str, display_name: str=None, tooltip: str=None): - super().__init__(id, display_name, tooltip) + pass class DynamicInput(InputV3, io_type=None): @@ -202,20 +268,74 @@ class AutoGrowDynamicInput(DynamicInput, io_type="COMFY_MULTIGROW_V3"): assert(max >= 1) self.min = min self.max = max - -AutoGrowDynamicInput(id="dynamic", template_input=ImageInput(id="image")) - - -class Hidden: - def __init__(self, hidden_var: str): - self.hidden_var = hidden_var - - class ComboDynamicInput(DynamicInput, io_type="COMFY_COMBODYNAMIC_V3"): def __init__(self, id: str): pass +AutoGrowDynamicInput(id="dynamic", template_input=ImageInput(id="image")) + + +class Hidden(str, Enum): + ''' + Enumerator for requesting hidden variables in nodes. + ''' + + 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).""" + prompt = "PROMPT" + """PROMPT is the complete prompt sent by the client to the server. See the prompt object for a full description.""" + extra_pnginfo = "EXTRA_PNGINFO" + """EXTRA_PNGINFO is a dictionary that will be copied into the metadata of any .png files saved. Custom nodes can store additional information in this dictionary for saving (or as a way to communicate with a downstream node).""" + dynprompt = "DYNPROMPT" + """DYNPROMPT is an instance of comfy_execution.graph.DynamicPrompt. It differs from PROMPT in that it may mutate during the course of execution in response to Node Expansion.""" + auth_token_comfy_org = "AUTH_TOKEN_COMFY_ORG" + """AUTH_TOKEN_COMFY_ORG is a token acquired from signing into a ComfyOrg account on frontend.""" + api_key_comfy_org = "API_KEY_COMFY_ORG" + """API_KEY_COMFY_ORG is an API Key generated by ComfyOrg that allows skipping signing into a ComfyOrg account on frontend.""" + + # ''' + # Request hidden value based on hidden_var key. + # ''' + # def __init__(self, hidden_var: str): + # self.hidden_var = hidden_var + +# NOTE: does this exist? +# class HiddenNodeId(Hidden): +# """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).""" +# def __init__(self): +# super().__init__("NODE_ID") + +# class HiddenUniqueId(Hidden): +# """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).""" +# def __init__(self): +# super().__init__("UNIQUE_ID") + +# class HiddenPrompt(Hidden): +# """PROMPT is the complete prompt sent by the client to the server. See the prompt object for a full description.""" +# def __init__(self): +# super().__init__("PROMPT") + +# class HiddenExtraPngInfo(Hidden): +# """EXTRA_PNGINFO is a dictionary that will be copied into the metadata of any .png files saved. Custom nodes can store additional information in this dictionary for saving (or as a way to communicate with a downstream node).""" +# def __init__(self): +# super().__init__("EXTRA_PNGINFO") + +# class HiddenDynPrompt(Hidden): +# """DYNPROMPT is an instance of comfy_execution.graph.DynamicPrompt. It differs from PROMPT in that it may mutate during the course of execution in response to Node Expansion.""" +# def __init__(self): +# super().__init__("DYNPROMPT") + +# class HiddenAuthTokenComfyOrg(Hidden): +# """Token acquired from signing into a ComfyOrg account on frontend.""" +# def __init__(self): +# super().__init__("AUTH_TOKEN_COMFY_ORG") + +# class HiddenApiKeyComfyOrg(Hidden): +# """API Key generated by ComfyOrg that allows skipping signing into a ComfyOrg account on frontend.""" +# def __init__(self): +# super().__init__("API_KEY_COMFY_ORG") + # class HiddenParam: # def __init__(self): @@ -229,34 +349,75 @@ class ComboDynamicInput(DynamicInput, io_type="COMFY_COMBODYNAMIC_V3"): # return type(f"{hidden_var}_HIDDEN", (HiddenParam,), {}, hidden_var=hidden_var) -Hidden("AUTH_KEY") - - class SchemaV3: def __init__(self, - inputs: list[InputV3], category: str, + inputs: list[InputV3], outputs: list[OutputV3]=None, - hidden: list[Hidden]=None, + hidden: list[Hidden]=None, + description: str="", + is_input_list: bool = False, is_output_node: bool=False, is_deprecated: bool=False, is_experimental: bool=False, + is_api_node: bool=False, ): - self.inputs = inputs self.category = category + """The category of the node, as per the "Add Node" menu.""" + self.inputs = inputs self.outputs = outputs self.hidden = hidden + self.description = description + """Node description, shown as a tooltip when hovering over the node.""" + self.is_input_list = is_input_list + """A flag indicating if this node implements the additional code necessary to deal with OUTPUT_IS_LIST nodes. + + All inputs of ``type`` will become ``list[type]``, regardless of how many items are passed in. This also affects ``check_lazy_status``. + + From the docs: + + A node can also override the default input behaviour and receive the whole list in a single call. This is done by setting a class attribute `INPUT_IS_LIST` to ``True``. + + Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lists#list-processing + """ self.is_output_node = is_output_node + """Flags this node as an output node, causing any inputs it requires to be executed. + + If a node is not connected to any output nodes, that node will not be executed. Usage:: + + OUTPUT_NODE = True + + From the docs: + + By default, a node is not considered an output. Set ``OUTPUT_NODE = True`` to specify that it is. + + Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview#output-node + """ self.is_deprecated = is_deprecated + """Flags a node as deprecated, indicating to users that they should find alternatives to this node.""" self.is_experimental = is_experimental + """Flags a node as experimental, informing users that it may change or not work as expected.""" + self.is_api_node = is_api_node + """Flags a node as an API node. See: https://docs.comfy.org/tutorials/api-nodes/overview.""" class ComfyNodeV3(ABC): - SCHEMA = None - def __init__(self, schema=SCHEMA): - if schema is None: - raise Exception("No schema was defined for this node!") + @classmethod + def GET_SCHEMA(cls) -> SchemaV3: + """ + Override this function with one that returns a SchemaV3 instance. + """ + return None + GET_SCHEMA = None + + def __init__(self): + if self.GET_SCHEMA is None: + raise Exception("No GET_SCHEMA function was defined for this node.") + + @abstractmethod + def execute(self, inputs, outputs, hidden, **kwargs): + pass # @classmethod # @abstractmethod @@ -271,10 +432,6 @@ class ComfyNodeV3(ABC): # @abstractmethod # def execute(self, inputs, outputs, hidden): # pass - - -class TestNode(ComfyNodeV3): - pass # class ComfyNodeV3: @@ -299,6 +456,14 @@ class TestNode(ComfyNodeV3): # IO_TYPE = IO_TYPE +# class AnimateDiffModelInput(InputV3, io_type="MODEL_M"): +# def __init__(self): +# pass + +# def execute(inputs, outputs, hidden): +# pass + + class ReturnedInputs: def __init__(self): pass @@ -308,27 +473,46 @@ class ReturnedOutputs: pass -class AnimateDiffModelInput(InputV3, io_type="MODEL_M"): +class NodeOutputV3: def __init__(self): pass - def execute(inputs, outputs, hidden): +class UINodeOutput: + def __init__(self): + pass + + +class TestNode(ComfyNodeV3): + SCHEMA = SchemaV3( + category="v3_test", + inputs=[], + ) + + # @classmethod + # def GET_SCHEMA(cls): + # return cls.SCHEMA + + @classmethod + def GET_SCHEMA(cls): + return cls.SCHEMA + + def execute(**kwargs): pass if __name__ == "__main__": print("hello there") inputs: list[InputV3] = [ IntegerInput("my_int"), - CustomInput("XYZ", id="xyz"), - CustomInput("MODEL_M", id="model1"), + CustomInput("xyz", "XYZ"), + CustomInput("model1", "MODEL_M"), ImageInput("my_image"), FloatInput("my_float"), - MultitypedInput("my_inputs", [CustomType("MODEL_M")]), + MultitypedInput("my_inputs", [CustomType("MODEL_M"), CustomType("XYZ")]), ] outputs: list[OutputV3] = [ ImageOutput("image"), - CustomOutput("XYZ", "xyz") + CustomOutput("xyz", "XYZ") ] for c in inputs: @@ -340,4 +524,4 @@ if __name__ == "__main__": for c in outputs: print(f"{c}, {type(c)}, {type(c).io_type}, {c.id}") - zzz = TestNode() \ No newline at end of file + zzz = TestNode()