diff --git a/custom-nodes/intro.mdx b/custom-nodes/intro.mdx index 7f36c9b92..b990c51ae 100644 --- a/custom-nodes/intro.mdx +++ b/custom-nodes/intro.mdx @@ -22,6 +22,6 @@ Custom Nodes extend ComfyUI with new server-side behavior, user-interface featur ## Choose a starting point - **New to custom nodes:** start with [Custom Nodes Overview](/custom-nodes/overview). -- **Building the backend:** see [Python (Backend)](/custom-nodes/backend/server_overview). +- **Building the backend:** see [Python (Backend) · V3](/custom-nodes/v3/backend/server_overview). New nodes should use the V3 schema; find the [legacy V1 schema](/custom-nodes/backend/server_overview) in the V1 section. - **Extending the UI:** see [JavaScript (UI)](/custom-nodes/js/javascript_overview). - **Publishing a package:** start with the [Registry Overview](/registry/overview). diff --git a/custom-nodes/overview.mdx b/custom-nodes/overview.mdx index 3039a23e3..914ba8c29 100644 --- a/custom-nodes/overview.mdx +++ b/custom-nodes/overview.mdx @@ -54,8 +54,8 @@ In a small number of cases, the UI features and the server need to interact with Build and register a working custom node, step by step. - - The node class contract: properties, inputs, return types. + + The V3 node class contract: properties, inputs, return types. Extend the ComfyUI frontend from your node package. diff --git a/custom-nodes/v3/backend/datatypes.mdx b/custom-nodes/v3/backend/datatypes.mdx new file mode 100644 index 000000000..255ce9c34 --- /dev/null +++ b/custom-nodes/v3/backend/datatypes.mdx @@ -0,0 +1,214 @@ +--- +title: "ComfyUI Custom Node Datatypes (V3)" +description: "Reference for ComfyUI custom node data types: Python types, tensor formats (IMAGE, LATENT, MASK), custom types, and wildcards for V3 custom nodes." +--- + +These are the most important built in datatypes. You can also [define your own](./more_on_inputs#custom-datatypes). + +In the V3 schema every datatype is a class in the `io` module, such as `io.Image` or `io.Int`. You declare +an input with its `.Input(...)` constructor and an output with `.Output(...)`. See the +[V3 Migration guide](/custom-nodes/v3_migration) for the full mapping between the V1 type strings and the +`io` classes. + +Datatypes are used on the client side to prevent a workflow from passing the wrong form of data into a +node - a bit like strong typing. The JavaScript client side code will generally not allow a node output to +be connected to an input of a different datatype, although a few exceptions are noted below. + +## Comfy datatypes + +### COMBO + +`io.Combo` represents a dropdown menu widget. The value of the input is a `str`; the widget options are +provided with the `options` parameter. + +```python +io.Combo.Input("play_sound", options=["no", "yes"]) +``` + +`COMBO` inputs are often dynamically generated at run time. Because `define_schema` is a class method, the +options can be computed when the node is loaded. For instance, a checkpoint loader node might do: + +```python +io.Combo.Input("ckpt_name", options=folder_paths.get_filename_list("checkpoints")) +``` + +There is also `io.MultiCombo` for dropdowns where more than one option can be selected; its value is a +`list[str]`. + +### Primitive and reroute + +Primitive and reroute nodes only exist on the client side. They do not have an intrinsic datatype, but +when connected they take on the datatype of the input or output to which they have been connected (which +is why they can't connect to a `*` input...) + +## Python datatypes + +### INT + +`io.Int` is an integer widget input. + +* Parameters: `default`, `min`, `max`, `step` (all optional) + +* Python datatype: `int` + +```python +io.Int.Input("count", default=1, min=0, max=4096, step=1) +``` + +### FLOAT + +`io.Float` is a float widget input. + +* Parameters: `default`, `min`, `max`, `step` (all optional) + +* Python datatype: `float` + +```python +io.Float.Input("strength", default=1.0, min=0.0, max=10.0, step=0.1) +``` + +### STRING + +`io.String` is a text widget input. + +* Parameters: `default`, `multiline`, `placeholder`, `dynamic_prompts` (all optional) + +* Python datatype: `str` + +```python +io.String.Input("text", default="Hello", multiline=True) +``` + +### BOOLEAN + +`io.Boolean` is a toggle widget input. + +* Parameters: `default`, `label_on`, `label_off` (all optional) + +* Python datatype: `bool` + +```python +io.Boolean.Input("enabled", default=True, label_on="On", label_off="Off") +``` + +## Tensor datatypes + +### IMAGE + +* Declared with `io.Image.Input(...)` + +* Python datatype: `torch.Tensor` with *shape* \[B,H,W,C] + +A batch of `B` images, height `H`, width `W`, with `C` channels (generally `C=3` for `RGB`). + +### LATENT + +* Declared with `io.Latent.Input(...)` + +* Python datatype: `dict`, containing a `torch.Tensor` with *shape* \[B,C,H,W] + +The `dict` passed contains the key `samples`, which is a `torch.Tensor` with *shape* \[B,C,H,W] representing +a batch of `B` latents, with `C` channels (generally `C=4` for existing stable diffusion models), height `H`, width `W`. + +The height and width are 1/8 of the corresponding image size (which is the value you set in the Empty Latent Image node). + +Other entries in the dictionary contain things like latent masks. + +{/* TODO need to dig into this */} + +{/* TODO new SD models might have different C values? */} + +### MASK + +* Declared with `io.Mask.Input(...)` + +* Python datatype: `torch.Tensor` with *shape* \[H,W] or \[B,C,H,W] + +### AUDIO + +* Declared with `io.Audio.Input(...)` + +* Python datatype: `dict`, containing a `torch.Tensor` with *shape* \[B, C, T] and a sample rate. + +The `dict` passed contains the key `waveform`, which is a `torch.Tensor` with *shape* \[B, C, T] representing a batch of `B` audio samples, with `C` channels (`C=2` for stereo and `C=1` for mono), and `T` time steps (i.e., the number of audio samples). + +The `dict` contains another key `sample_rate`, which indicates the sampling rate of the audio. + +## Custom Sampling datatypes + +### Noise + +The `NOISE` datatype represents a *source* of noise (not the actual noise itself). It can be represented by any Python object +that provides a method to generate noise, with the signature `generate_noise(self, input_latent:Tensor) -> Tensor`, and a +property, `seed:Optional[int]`. + +The `seed` is passed into `sample` guider in the `SamplerCustomAdvanced`, but does not appear to be used in any of the standard guiders. +It is Optional, so you can generally set it to None. + +When noise is to be added, the latent is passed into this method, which should return a `Tensor` of the same shape containing the noise. + +See the [noise mixing example](/custom-nodes/backend/snippets#creating-noise-variations) + +### Sampler + +The `SAMPLER` datatype represents a sampler, which is represented as a Python object providing a `sample` method. +Stable diffusion sampling is beyond the scope of this guide; see `comfy/samplers.py` if you want to dig into this part of the code. + +### Sigmas + +The `SIGMAS` datatypes represents the values of sigma before and after each step in the sampling process, as produced by a scheduler. +This is represented as a one-dimensional tensor, of length `steps+1`, where each element represents the noise expected to be present +before the corresponding step, with the final value representing the noise present after the final step. + +A `normal` scheduler, with 20 steps and denoise of 1, for an SDXL model, produces: + +``` +tensor([14.6146, 10.7468, 8.0815, 6.2049, 4.8557, + 3.8654, 3.1238, 2.5572, 2.1157, 1.7648, + 1.4806, 1.2458, 1.0481, 0.8784, 0.7297, + 0.5964, 0.4736, 0.3555, 0.2322, 0.0292, 0.0000]) +``` + +The starting value of sigma depends on the model, which is why a scheduler node requires a `MODEL` input to produce a SIGMAS output + +### Guider + +A `GUIDER` is a generalisation of the denoising process, as 'guided' by a prompt or any other form of conditioning. In Comfy the guider is +represented by a `callable` Python object providing a `__call__(*args, **kwargs)` method which is called by the sample. + +The `__call__` method takes (in `args[0]`) a batch of noisy latents (tensor `[B,C,H,W]`), and returns a prediction of the noise (a tensor of the same shape). + +## Model datatypes + +There are a number of more technical datatypes for stable diffusion models. The most significant ones are `io.Model`, `io.CLIP`, +`io.VAE` and `io.Conditioning`. +Working with these is (for the time being) beyond the scope of this guide! {/* TODO but maybe not forever */} + +## Additional Parameters + +The `Input` constructors of the widget datatypes accept a number of optional parameters that configure the +widget. Below is a list of the officially supported parameters. (The legacy V1 schema spelled these as keys +in the input options dictionary, e.g. `forceInput` and `rawLink`; in V3 they are snake_case parameters of +the `Input` constructors.) + +You can use additional keys for your own custom widgets, but should *not* reuse any of the parameters below for other purposes. + +| Parameter | Applies to | Description | +| --------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `default` | widget inputs | The default value of the widget | +| `min` | `io.Int`, `io.Float` | The minimum value of the widget | +| `max` | `io.Int`, `io.Float` | The maximum value of the widget | +| `step` | `io.Int`, `io.Float` | The amount to increment or decrement a widget | +| `multiline` | `io.String` | Use a multiline text box | +| `placeholder` | `io.String` | Placeholder text to display in the UI when empty | +| `dynamic_prompts` | `io.String` | Causes the front-end to evaluate dynamic prompts | +| `options` | `io.Combo`, `io.MultiCombo` | The dropdown options | +| `label_on`, `label_off` | `io.Boolean` | The labels to use in the UI when the bool is `True` / `False` | +| `control_after_generate` | `io.Int`, `io.Combo` | Adds a "control after generate" widget (pass `True`, or an `io.ControlAfterGenerate` enum value) | +| `socketless` | widget inputs | Hide the input socket, so the widget cannot be connected to an upstream node | +| `force_input` | widget inputs | Display the input as a socket instead of a widget, and don't allow converting it back to a widget | +| `optional` | all inputs | Declares the input optional, so it does not have to be connected | +| `lazy` | all inputs | Declares that this input uses [Lazy Evaluation](./lazy_evaluation) | +| `raw_link` | all inputs | When a link exists, rather than receiving the evaluated value, you will receive the link (i.e. `["nodeId", ]`). Primarily useful when your node uses [Node Expansion](/custom-nodes/backend/expansion). | +| `tooltip` | all inputs | Tooltip text to display when hovering over the input | +| `advanced` | all inputs | Hides the input behind an "Advanced" toggle in the UI | diff --git a/custom-nodes/v3/backend/lazy_evaluation.mdx b/custom-nodes/v3/backend/lazy_evaluation.mdx new file mode 100644 index 000000000..9852ba673 --- /dev/null +++ b/custom-nodes/v3/backend/lazy_evaluation.mdx @@ -0,0 +1,184 @@ +--- +title: "Lazy Evaluation (V3)" +description: "Learn how lazy evaluation works in ComfyUI (v0.2.0+). Discover how to defer input evaluation, optimize VRAM usage, and implement lazy inputs in V3 custom nodes." +--- + +## Lazy Evaluation + +By default, all inputs are evaluated before a node can be run. Sometimes, however, an input won't +necessarily be used and evaluating it would result in unnecessary processing. Here are some examples of +nodes where lazy evaluation may be beneficial: + +1. A `ModelMergeSimple` node where the ratio is either `0.0` (in which case the first model doesn't need to be loaded) or `1.0` (in which case the second model doesn't need to be loaded). +2. Interpolation between two images where the ratio (or mask) is either entirely `0.0` or entirely `1.0`. +3. A Switch node where one input determines which of the other inputs will be passed through. + +There is very little cost in making an input lazy. If it's something you can do, you generally should. + +### Creating Lazy Inputs + +There are two steps to making an input a "lazy" input. They are: + +1. Mark the input as lazy in the schema, by passing `lazy=True` to its `Input` definition +2. Define a class method named `check_lazy_status` that will be called prior to evaluation to determine if any more inputs are necessary. + +To demonstrate these, we'll make a "MixImages" node that interpolates between two images according to a +mask. If the entire mask is `0.0`, we don't need to evaluate any part of the tree leading up to the second +image. If the entire mask is `1.0`, we can skip evaluating the first image. + +#### Defining the schema + +Declaring that an input is lazy is as simple as passing `lazy=True` to the input's definition. + +```python +from comfy_api.latest import io + + +class LazyMixImages(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="LazyMixImages", + display_name="Lazy Mix Images", + category="examples", + inputs=[ + io.Image.Input("image1", lazy=True), + io.Image.Input("image2", lazy=True), + io.Mask.Input("mask"), + ], + outputs=[ + io.Image.Output(), + ], + ) +``` + +In this example, `image1` and `image2` are both marked as lazy inputs, but `mask` will always be evaluated. + +#### Defining `check_lazy_status` + +A `check_lazy_status` method is called if there are one or more lazy inputs that are not yet available. It +receives the same arguments as `execute`. All available inputs are passed in with their final values while +unavailable lazy inputs have a value of `None`. + +When a lazy input was defined with `INPUT_IS_LIST = True`, an unevaluated input is passed to +`check_lazy_status` as `(None,)` rather than `None`, so an `is None` check would miss it. Instead, check for +the `(None,)` sentinel to ensure required inputs are not omitted. + +The responsibility of `check_lazy_status` is to return a list of the names of any lazy inputs that are +needed to proceed. If all lazy inputs are available, the function should return an empty list. + +Note that `check_lazy_status` may be called multiple times. (For example, you might find after evaluating +one lazy input that you need to evaluate another.) + +In V3 `check_lazy_status` is a class method, like `execute`. (In the legacy V1 schema it was a plain method.) + +```python +@classmethod +def check_lazy_status(cls, mask, image1=None, image2=None): + mask_min = float(mask.min()) + mask_max = float(mask.max()) + needed = [] + if image1 is None and not (mask_min == 1.0 and mask_max == 1.0): + needed.append("image1") + if image2 is None and not (mask_min == 0.0 and mask_max == 0.0): + needed.append("image2") + return needed + +@classmethod +def execute(cls, mask, image1=None, image2=None) -> io.NodeOutput: + mask_min = float(mask.min()) + mask_max = float(mask.max()) + if mask_min == 0.0 and mask_max == 0.0: + return io.NodeOutput(image1) + if mask_min == 1.0 and mask_max == 1.0: + return io.NodeOutput(image2) + # Not trying to handle different batch sizes here just to keep the demo simple + return io.NodeOutput(image1 * (1.0 - mask) + image2 * mask) +``` + +### Full Example + +```python +from comfy_api.latest import ComfyExtension, io + + +class LazyMixImages(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="LazyMixImages", + display_name="Lazy Mix Images", + category="examples", + inputs=[ + io.Image.Input("image1", lazy=True), + io.Image.Input("image2", lazy=True), + io.Mask.Input("mask"), + ], + outputs=[ + io.Image.Output(), + ], + ) + + @classmethod + def check_lazy_status(cls, mask, image1=None, image2=None): + mask_min = float(mask.min()) + mask_max = float(mask.max()) + needed = [] + if image1 is None and not (mask_min == 1.0 and mask_max == 1.0): + needed.append("image1") + if image2 is None and not (mask_min == 0.0 and mask_max == 0.0): + needed.append("image2") + return needed + + @classmethod + def execute(cls, mask, image1=None, image2=None) -> io.NodeOutput: + mask_min = float(mask.min()) + mask_max = float(mask.max()) + if mask_min == 0.0 and mask_max == 0.0: + return io.NodeOutput(image1) + if mask_min == 1.0 and mask_max == 1.0: + return io.NodeOutput(image2) + # Not trying to handle different batch sizes here just to keep the demo simple + return io.NodeOutput(image1 * (1.0 - mask) + image2 * mask) + + +class ExampleExtension(ComfyExtension): + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [LazyMixImages] + + +async def comfy_entrypoint() -> ExampleExtension: + return ExampleExtension() +``` + +## Execution Blocking + +While Lazy Evaluation is the recommended way to "disable" part of a graph, there are times when you want to +disable a node that doesn't implement lazy evaluation itself. If it's an output node that you developed +yourself, you should just add lazy evaluation as follows: + +1. Add a required (if this is a new node) or optional (if you care about backward compatibility) input for `enabled` that defaults to `True` +2. Make all other inputs lazy inputs +3. Only evaluate the other inputs if `enabled` is `True` + +To block execution from a node whose output may be invalid or meaningless, return an +`io.NodeOutput` with a `block_execution` message. Comfy replaces every output of the node with an +`ExecutionBlocker` carrying that message. Any nodes which receive an `ExecutionBlocker` as input will skip +execution and return that `ExecutionBlocker` for any outputs, so the message is reported to the user when a +blocked output is actually used. + +```python +@classmethod +def execute(cls, ckpt_name) -> io.NodeOutput: + ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) + model, clip, vae = load_checkpoint(ckpt_path) + if vae is None: + # This error is more useful than a "'NoneType' has no attribute" error + # in a later node + return io.NodeOutput( + block_execution=f"No VAE contained in the loaded model {ckpt_name}" + ) + return io.NodeOutput(model, clip, vae) +``` + +**There is intentionally no way to stop an ExecutionBlocker from propagating forward.** If you think you want this, you should really be using Lazy Evaluation. diff --git a/custom-nodes/v3/backend/lifecycle.mdx b/custom-nodes/v3/backend/lifecycle.mdx new file mode 100644 index 000000000..fb9a108d4 --- /dev/null +++ b/custom-nodes/v3/backend/lifecycle.mdx @@ -0,0 +1,75 @@ +--- +title: "Lifecycle (V3)" +description: "Understand how ComfyUI loads custom nodes: the comfy_entrypoint function and ComfyExtension class that register V3 nodes, plus WEB_DIRECTORY for deploying client-side JavaScript." +--- + +## How Comfy loads custom nodes + +When Comfy starts, it scans the directory `custom_nodes` for Python modules, and attempts to load them. +A module is treated as an extension when it exports a `comfy_entrypoint` function (the V3 schema) or a +`NODE_CLASS_MAPPINGS` dictionary (the legacy V1 schema). See the +[V3 Migration guide](/custom-nodes/v3_migration) for the differences between the two. + +A custom-node package is a directory containing an `__init__.py` file. +`__all__` affects wildcard imports only and does not control custom-node discovery. + +### __init__.py + +`__init__.py` is executed when Comfy attempts to import the module. If the import succeeds, Comfy calls +the module's `comfy_entrypoint` function, which returns a `ComfyExtension` instance. The extension's +`get_node_list` method provides the node classes defined by the module, and those nodes become available +in Comfy. If there is an error in your code, Comfy will continue, but will report the module as having +failed to load. So check the Python console! + +A very simple `__init__.py` file would look like this: + +```python +from comfy_api.latest import ComfyExtension + +from .python_file import MyCustomNode + + +class MyExtension(ComfyExtension): + async def get_node_list(self) -> list[type[MyCustomNode]]: + return [MyCustomNode] + + +async def comfy_entrypoint() -> MyExtension: + return MyExtension() +``` + +#### comfy_entrypoint + +`comfy_entrypoint` is the function Comfy calls to discover the nodes in your module. It may be declared +`async` or not, but it must return an instance of `ComfyExtension`. The `get_node_list` method on the +extension must be `async`, and returns the list of node classes the extension provides. + +The `node_id` of each node (its unique name across the Comfy install) and its display name, category and +other properties are defined in the node class's `define_schema` method, rather than in the +`__init__.py`. See [Properties](./server_overview) for the schema fields. + +#### NODE_CLASS_MAPPINGS (legacy V1) + +Modules written against the legacy V1 schema can still export `NODE_CLASS_MAPPINGS`, a `dict` mapping +each custom node name (unique across the Comfy install) to its node class. Comfy loads those modules +without calling `comfy_entrypoint`. + +```python +from .python_file import MyCustomNode +NODE_CLASS_MAPPINGS = {"MyCustomNode": MyCustomNode} +``` + +`NODE_DISPLAY_NAME_MAPPINGS` was the legacy way to give a node a display name different from its unique +name. In V3 this is done with the `display_name` field of the schema. + +#### WEB_DIRECTORY + +If you are deploying client side code, you will also need to export the path, relative to the module, in +which the JavaScript files are to be found. It is conventional to place these in a subdirectory of your +custom node named `js`. Comfy also registers a web directory automatically when your `pyproject.toml` +contains a `[tool.comfy]` section with a `web` key pointing at one. + +*Only* `.js` files will be served; you can't deploy `.css` or other types in this way + +In previous versions of Comfy, `__init__.py` was required to copy the JavaScript files into the main Comfy web +subdirectory. You will still see code that does this. Don't. diff --git a/custom-nodes/v3/backend/lists.mdx b/custom-nodes/v3/backend/lists.mdx new file mode 100644 index 000000000..cebc5a097 --- /dev/null +++ b/custom-nodes/v3/backend/lists.mdx @@ -0,0 +1,83 @@ +--- +title: "Data lists (V3)" +description: "Learn how ComfyUI handles data as Python lists internally, including length-one processing, list processing for batches, and how to use the V3 is_input_list and is_output_list schema options when developing custom nodes." +--- + +## Length one processing + +Internally, the Comfy server represents data flowing from one node to the next as a Python `list`, normally +length 1, of the relevant datatype. In normal operation, when a node returns an output, each element in the +output is separately wrapped in a list (length 1); then when the next node is called, the data is unwrapped +and passed to the `execute` method. + +You generally don't need to worry about this, since Comfy does the wrapping and unwrapping. + +This isn't about batches. A batch (of, for instance, latents, or images) is a *single entry* in the list (see [tensor datatypes](/custom-nodes/backend/images_and_masks)) + +## List processing + +In some circumstance, multiple data instances are processed in a single workflow, in which case the internal +data will be a list containing the data instances. An example of this might be processing a series of images +one at a time to avoid running out of VRAM, or handling images of different sizes. + +By default, Comfy will process the values in the list sequentially: + +- if the inputs are `list`s of different lengths, the shorter ones are padded by repeating the last value +- the `execute` method is called once for each value in the input lists +- the outputs are `list`s, each of which is the same length as the longest input + +The relevant code can be found in the method `map_node_over_list` in `execution.py`. + +However, as Comfy wraps node outputs into a `list` of length one, if the values returned by a custom node +contain a `list`, that `list` will be wrapped, and treated as a single piece of data. + +Two V3 schema options change this behaviour: + +- `is_input_list=True` on the schema: the node receives the *whole* list in a single call, instead of being + called once per item. All inputs become `list[type]`, regardless of how many items are passed in. This + replaces the legacy V1 class attribute `INPUT_IS_LIST`. +- `is_output_list=True` on an output: the list returned for that output is not wrapped, and is treated as a + series of data for sequential processing by downstream nodes. This replaces the legacy V1 class attribute + `OUTPUT_IS_LIST`. + +To show how the two options work together, here's an `ImageRebatch`-style node written in the V3 schema. It +takes one or more batches of images (received as a list, because `is_input_list=True`) and rebatches them +into batches of the requested size: + +`is_input_list` is node level - all inputs get the same treatment. So the value of the `batch_size` widget is given by `batch_size[0]`. + +```python +from comfy_api.latest import ComfyExtension, io + + +class ImageRebatch(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="ImageRebatch", + category="image/batch", + inputs=[ + io.Image.Input("images"), + io.Int.Input("batch_size", default=1, min=1, max=4096), + ], + outputs=[ + io.Image.Output(is_output_list=True), + ], + is_input_list=True, + ) + + @classmethod + def execute(cls, images, batch_size) -> io.NodeOutput: + batch_size = batch_size[0] # everything comes as a list, so batch_size is list[int] + + output_list = [] + all_images = [] + for img in images: # each img is a batch of images + for i in range(img.shape[0]): # each i is a single image + all_images.append(img[i:i+1]) + + for i in range(0, len(all_images), batch_size): # take batch_size chunks and turn each into a new batch + output_list.append(torch.cat(all_images[i:i+batch_size], dim=0)) # will die horribly if the image batches had different width or height! + + return io.NodeOutput(output_list) +``` diff --git a/custom-nodes/v3/backend/more_on_inputs.mdx b/custom-nodes/v3/backend/more_on_inputs.mdx new file mode 100644 index 000000000..357706960 --- /dev/null +++ b/custom-nodes/v3/backend/more_on_inputs.mdx @@ -0,0 +1,152 @@ +--- +title: "Hidden and Flexible inputs (V3)" +description: "ComfyUI hidden inputs (UNIQUE_ID, PROMPT, EXTRA_PNGINFO, DYNPROMPT) and flexible inputs, including custom datatypes and wildcard inputs for custom nodes (V3 schema)." +--- + +## Hidden inputs + +Alongside the inputs declared in the schema, which create corresponding inputs or widgets on the +client-side, custom nodes can request certain information from the server through *hidden* inputs. Hidden +inputs are not visible in the UI. + +In the V3 schema, hidden inputs are requested by passing a list of `io.Hidden` enum values to the `hidden` +parameter of the schema. During execution, the values are available on the node class as `cls.hidden`, so +an `execute` method can read them by name: + +```python +from comfy_api.latest import io + +class MyNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="MyNode", + inputs=[io.String.Input("text")], + outputs=[io.String.Output()], + hidden=[ + io.Hidden.unique_id, + io.Hidden.prompt, + io.Hidden.extra_pnginfo, + ], + ) + + @classmethod + def execute(cls, text) -> io.NodeOutput: + # hidden values are accessed via cls.hidden + print(cls.hidden.unique_id) + print(cls.hidden.prompt) + print(cls.hidden.extra_pnginfo) + return io.NodeOutput(text) +``` + +Available hidden values (see the [V3 Migration guide](/custom-nodes/v3_migration) for the full list): + +### UNIQUE_ID + +`io.Hidden.unique_id` provides 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](/development/comfyui-server/comms_messages#getting-node-id)). + +### PROMPT + +`io.Hidden.prompt` provides the complete prompt sent by the client to the server. See +[the prompt object](/custom-nodes/js/javascript_objects_and_hijacking#prompt) for a full description. + +### EXTRA_PNGINFO + +`io.Hidden.extra_pnginfo` provides 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). + +Note that if Comfy is started with the `disable_metadata` option, this data won't be saved. + +### DYNPROMPT + +`io.Hidden.dynprompt` provides 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](/custom-nodes/backend/expansion). + +`DYNPROMPT` should only be used for advanced cases (like implementing loops in custom nodes). + +## Flexible inputs + +### Custom datatypes + +If you want to pass data between your own custom nodes, you may find it helpful to define a custom +datatype. This is (almost) as simple as just choosing a name for the datatype, which should be a unique +string in upper case, such as `CHEESE`. + +Create the type with the `io.Custom` helper (or the `@io.comfytype` decorator for a full class +definition), then use it for inputs and outputs. The Comfy client will only allow `CHEESE` outputs to +connect to a `CHEESE` input. A `CHEESE` value can be any Python object. + +```python +from comfy_api.latest import io + +# Create the custom type once, then reuse it +Cheese = io.Custom("CHEESE") + +class CheeseNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="CheeseNode", + inputs=[Cheese.Input("my_cheese")], + outputs=[Cheese.Output()], + ) +``` + +Because the Comfy client doesn't know anything about `CHEESE`, it can't display a widget for it. Custom +datatype inputs are therefore socket inputs: they must be connected to an upstream node, and the widget +conversion options (`force_input` and `socketless`) don't apply to them. If you want a widget fallback for +your custom type, you need to define a custom widget for it, which is a topic for another day. + +### Wildcard inputs + +The frontend allows `*` to indicate that an input can be connected to any source. In the V3 schema, +`io.AnyType` provides this wildcard type directly: + +```python +class AnyNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="AnyNode", + inputs=[ + io.AnyType.Input("anything"), + ], + outputs=[io.AnyType.Output()], + ) +``` + +With `io.AnyType`, type validation accepts any input, so the legacy V1 workaround (adding an +`input_types` argument to `VALIDATE_INPUTS` in order to skip backend validation) is not needed. It's up to +the node to make sense of the data that is passed. + +### Dynamically created inputs + +If inputs are dynamically created on the client side, they can't be defined in the Python source code. + +The V3 schema handles this with the `accept_all_inputs` flag. When it is `True`, all inputs from the +prompt that are not defined in the schema are passed through to `execute` as keyword arguments: + +```python +class FlexibleNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="FlexibleNode", + inputs=[ + # inputs that are always present can still be declared here + ], + outputs=[io.Image.Output()], + accept_all_inputs=True, + ) + + @classmethod + def execute(cls, **kwargs) -> io.NodeOutput: + # the dynamically created input data will be in the keyword arguments + ... +``` + +Note that inputs declared in the schema are still validated and dispatched normally; `accept_all_inputs` +only affects the inputs that are *not* declared. diff --git a/custom-nodes/v3/backend/server_overview.mdx b/custom-nodes/v3/backend/server_overview.mdx new file mode 100644 index 000000000..857cf5474 --- /dev/null +++ b/custom-nodes/v3/backend/server_overview.mdx @@ -0,0 +1,220 @@ +--- +title: "Properties (V3)" +description: "Properties of a custom node (V3 schema)." +--- + + +The node definitions on this page use the modern V3 schema (`io.ComfyNode` with `define_schema` and a `comfy_entrypoint` function), which is how the bundled [`custom_nodes/example_node.py.example`](https://github.com/comfyanonymous/ComfyUI/blob/master/custom_nodes/example_node.py.example) is written. The legacy V1 schema (`INPUT_TYPES` / `RETURN_TYPES`) is still fully supported. For the differences and how to migrate, see the [V3 Migration guide](/custom-nodes/v3_migration). + + +### Simple Example + +Here's the code for the Invert Image Node, which gives an overview of the key concepts in custom node development. + +```python +from comfy_api.latest import ComfyExtension, io + +class InvertImageNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="InvertImageNode", + display_name="Invert Image", + category="examples", + inputs=[ + io.Image.Input("image_in"), + ], + outputs=[ + io.Image.Output(display_name="image_out"), + ], + ) + + @classmethod + def execute(cls, image_in) -> io.NodeOutput: + image_out = 1 - image_in + return io.NodeOutput(image_out) + +class ExampleExtension(ComfyExtension): + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + InvertImageNode, + ] + +async def comfy_entrypoint() -> ExampleExtension: # ComfyUI calls this to load your extension and its nodes. + return ExampleExtension() +``` + +### Main properties + +Every custom node is a Python class inheriting from `io.ComfyNode`, with the following key properties: + +#### define_schema + +`define_schema`, as the name suggests, defines the schema for the node. The class method returns an `io.Schema` object which contains the node's metadata, inputs and outputs. + +The schema fields are: + +- `node_id`: a unique identifier for the node, used in the API and workflow JSON. +- `display_name`: the friendly name shown in the UI. Optional; defaults to `node_id`. +- `category`: where the node is found in the ComfyUI **Add Node** menu. Submenus can be specified as a path, eg. `examples/trivial`. +- `inputs`: a list of input objects. +- `outputs`: a list of output objects. +- `description`: the tooltip shown when hovering over the node. Optional. +- `search_aliases`: a list of alternative names users might search for when looking for this node. Optional. + +Each input is an object like `io.Int.Input("count", default=1, min=0, max=4096)`. The available input types are `io.Image`, `io.Mask`, `io.Model`, `io.VAE`, `io.CLIP`, `io.Conditioning`, `io.Latent`, `io.Audio`, `io.Int`, `io.Float`, `io.String`, `io.Combo`, `io.Boolean`, and more. See the [Datatypes](/custom-nodes/v3/backend/datatypes) page for details. + +As in V1, `define_schema` is a `@classmethod` so that widget options (like the name of the checkpoint to be loaded) can be computed at runtime. Let's go into this more later. + +Outputs are listed as `io.Image.Output()` and so on, with an optional `display_name` to label the output in the UI. + +#### execute + +The execution function is fixed to the name `execute`, and is a class method. It is called with named arguments matching the input ids defined in the schema. + +The function returns an `io.NodeOutput`, wrapping the result values. If the node has multiple outputs, pass them as multiple arguments: + +```python +return io.NodeOutput(model, clip, vae) +``` + +If the node has no outputs, return `io.NodeOutput()` (or simply nothing after processing side effects, but the return value must be `io.NodeOutput`, so `return` a bare `io.NodeOutput()`). + +### Execution Control Extras + +A great feature of Comfy is that it caches outputs, +and only executes nodes that might produce a different result than the previous run. +This can greatly speed up lots of workflows. + +In essence this works by identifying which nodes produce an output (these, notably the Image Preview and Save Image nodes, are always executed), and then working +backwards to identify which nodes provide data that might have changed since the last run. + +Two optional features of a custom node assist in this process. + +#### is_output_node + +By default, a node is not considered an output. Set `is_output_node=True` in the schema to specify that it is. + +```python +return io.Schema( + node_id="SaveImage", + ... + is_output_node=True, +) +``` + +#### fingerprint_inputs + +By default, Comfy considers that a node has changed if any of its inputs or widgets have changed. +This is normally correct, but you may need to override this if, for instance, the node uses a random +number (and does not specify a seed - it's best practice to have a seed input in this case so that +the user can control reproducibility and avoid unnecessary execution), or loads an input that may have +changed externally, or sometimes ignores inputs (so doesn't need to execute just because those inputs changed). + +`fingerprint_inputs` (formerly `IS_CHANGED` in V1) receives the same arguments as `execute` and returns a +value that Comfy compares with the one returned in the previous run. If the value differs, the node is executed. + +The name of this method was misleading in V1: `IS_CHANGED` is not "changed"; it is a cache key. Returning `True` every time makes the node run only once. + +A good example of actually checking for changes is the code from the built-in LoadImage node, which loads the image and returns a hash: + +```python + @classmethod + def fingerprint_inputs(s, image): + image_path = folder_paths.get_annotated_filepath(image) + m = hashlib.sha256() + with open(image_path, 'rb') as f: + m.update(f.read()) + return m.digest().hex() +``` + +To specify that your node should always be considered to have changed (which you should avoid if possible, since it +stops Comfy optimising what gets run), return a value that is never equal to the previous one, such as `float("NaN")`. + +#### not_idempotent + +If your node produces an output that depends on something other than its inputs (for example, a random number without a seed), set `not_idempotent=True` in the schema. This tells Comfy to skip the fast-path that assumes identical inputs produce identical outputs. + +### Other schema flags + +There are several other flags that can be used to modify how Comfy treats a node: + +- `is_deprecated`: flags the node as deprecated, telling users to find alternatives. +- `is_experimental`: flags the node as experimental, warning users that it may change. +- `is_input_list`: controls sequential processing of data, described [later](./lists). +- `hidden`: a list of hidden inputs (see [Hidden Inputs](./more_on_inputs#hidden-inputs)). +- `enable_expand`: allows the node to expand into a subgraph (see [Node Expansion](/custom-nodes/backend/expansion)). +- `accept_all_inputs`: passes all inputs that are not defined in the schema through to `execute` (see [Dynamically created inputs](./more_on_inputs#dynamically-created-inputs)). + +### validate_inputs + +If a class method `validate_inputs` is defined, it will be called before the workflow begins execution. +`validate_inputs` should return `True` if the inputs are valid, or a message (as a `str`) describing the error (which will prevent execution). + +#### Validating Constants + +Note that `validate_inputs` will only receive inputs that are defined as constants within the workflow. Any inputs that are received from other nodes will *not* be available in `validate_inputs`. + +`validate_inputs` is called with only the inputs that its signature requests (those returned by `inspect.getfullargspec(obj_class.validate_inputs).args`). Any inputs which are received in this way will *not* run through the default validation rules. + +For example, in the following snippet, the front-end will use the specified `min` and `max` values of the `foo` input, but the back-end will not enforce it. + +```python +from comfy_api.latest import io + +class CustomNode(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="CustomNode", + inputs=[ + io.Int.Input("foo", min=0, max=10), + ], + outputs=[], + ) + + @classmethod + def validate_inputs(cls, foo): + # YOLO, anything goes! + return True +``` + +Additionally, if the function takes a `**kwargs` input, it will receive *all* available inputs and all of them will skip validation as if specified explicitly. + +#### Validating Types + +If the `validate_inputs` method receives an argument named `input_types`, it will be passed a dictionary in which the key is the name of each input which is connected to an output from another node and the value is the type of that output. + +When this argument is present, all default validation of input types is skipped. Here's an example making use of the fact that the front-end allows for the specification of multiple types: + +```python +from comfy_api.latest import io + +class AddNumbers(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="AddNumbers", + inputs=[ + io.MultiType.Input( + io.Int.Input("input1", min=0, max=1000), + types=[io.Int, io.Float], + ), + io.MultiType.Input( + io.Int.Input("input2", min=0, max=1000), + types=[io.Int, io.Float], + ), + ], + outputs=[io.Int.Output()], + ) + + @classmethod + def validate_inputs(cls, input_types): + # The min and max of input1 and input2 are still validated because + # we didn't take `input1` or `input2` as arguments + if input_types["input1"] not in (io.Int.io_type, io.Float.io_type): + return "input1 must be an INT or FLOAT type" + if input_types["input2"] not in (io.Int.io_type, io.Float.io_type): + return "input2 must be an INT or FLOAT type" + return True +``` diff --git a/docs.json b/docs.json index b6a227405..947363a1e 100644 --- a/docs.json +++ b/docs.json @@ -3424,7 +3424,7 @@ "custom-nodes/overview", "custom-nodes/walkthrough", { - "group": "Python (Backend)", + "group": "Python (Backend) · V1", "pages": [ "custom-nodes/backend/server_overview", "custom-nodes/backend/lifecycle", @@ -3439,6 +3439,17 @@ "development/comfyui-server/execution_model_inversion_guide" ] }, + { + "group": "Python (Backend) · V3", + "pages": [ + "custom-nodes/v3/backend/server_overview", + "custom-nodes/v3/backend/datatypes", + "custom-nodes/v3/backend/more_on_inputs", + "custom-nodes/v3/backend/lazy_evaluation", + "custom-nodes/v3/backend/lifecycle", + "custom-nodes/v3/backend/lists" + ] + }, { "group": "JavaScript (UI)", "pages": [ @@ -6656,7 +6667,7 @@ "zh/custom-nodes/overview", "zh/custom-nodes/walkthrough", { - "group": "Python(后端)", + "group": "Python(后端)· V1", "pages": [ "zh/custom-nodes/backend/server_overview", "zh/custom-nodes/backend/lifecycle", @@ -9961,7 +9972,7 @@ "ja/custom-nodes/overview", "ja/custom-nodes/walkthrough", { - "group": "Python(バックエンド)", + "group": "Python(バックエンド)· V1", "pages": [ "ja/custom-nodes/backend/server_overview", "ja/custom-nodes/backend/lifecycle", @@ -13179,7 +13190,7 @@ "ko/custom-nodes/overview", "ko/custom-nodes/walkthrough", { - "group": "Python (백엔드)", + "group": "Python (백엔드) · V1", "pages": [ "ko/custom-nodes/backend/server_overview", "ko/custom-nodes/backend/lifecycle", @@ -14364,4 +14375,4 @@ "destination": "/tutorials/partner-nodes/wan/wan3-0" } ] -} +} \ No newline at end of file