From 0844326e59bd541492a60f0f83e192bc43c2ba07 Mon Sep 17 00:00:00 2001 From: "Daxiong (Lin)" Date: Tue, 8 Sep 2026 22:04:01 +0800 Subject: [PATCH 1/2] docs(custom-nodes): add V3 schema docs into versioned /custom-nodes/v3 route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Keep all V1 backend docs in place (URLs unchanged, backward compatible) - Add custom-nodes/v3/* with 9 rewritten pages (walkthrough + 8 backend pages), based on the reviewed V3 rewrite (incl. CodeRabbit fixes) - Split the navigation group into 'Python (Backend) · V1' and 'Python (Backend) · V3' (en; zh/ja/ko V3 groups added once translations land) - V3 pages: title gets (V3) suffix, description marked V3, internal links point at v3 pages when available, else at the V1 page - Future versions slot in as /custom-nodes/v4/ etc. --- custom-nodes/v3/backend/datatypes.mdx | 214 +++++++++++++++++ custom-nodes/v3/backend/interface.mdx | 138 +++++++++++ custom-nodes/v3/backend/lazy_evaluation.mdx | 184 +++++++++++++++ custom-nodes/v3/backend/lifecycle.mdx | 75 ++++++ custom-nodes/v3/backend/lists.mdx | 83 +++++++ custom-nodes/v3/backend/manager.mdx | 76 ++++++ custom-nodes/v3/backend/more_on_inputs.mdx | 152 ++++++++++++ custom-nodes/v3/backend/server_overview.mdx | 220 +++++++++++++++++ custom-nodes/v3/walkthrough.mdx | 247 ++++++++++++++++++++ docs.json | 24 +- 10 files changed, 1408 insertions(+), 5 deletions(-) create mode 100644 custom-nodes/v3/backend/datatypes.mdx create mode 100644 custom-nodes/v3/backend/interface.mdx create mode 100644 custom-nodes/v3/backend/lazy_evaluation.mdx create mode 100644 custom-nodes/v3/backend/lifecycle.mdx create mode 100644 custom-nodes/v3/backend/lists.mdx create mode 100644 custom-nodes/v3/backend/manager.mdx create mode 100644 custom-nodes/v3/backend/more_on_inputs.mdx create mode 100644 custom-nodes/v3/backend/server_overview.mdx create mode 100644 custom-nodes/v3/walkthrough.mdx 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/interface.mdx b/custom-nodes/v3/backend/interface.mdx new file mode 100644 index 000000000..0683caa43 --- /dev/null +++ b/custom-nodes/v3/backend/interface.mdx @@ -0,0 +1,138 @@ +--- +title: "Code Interface (V3)" +description: "What are custom nodes and how are they used? (V3 schema)." +--- + +## Overview + +A custom node is a combination of Python code and potentially some models weights. Custom nodes are extremely powerful, and allows the Comfy community to build their own functionality into ComfyUI. + +If you prefer to read code, check out the [example](https://github.com/Comfy-Org/ComfyUI/blob/master/custom_nodes/example_node.py.example) in the repository. Otherwise, we will explore one of Comfy's built-in nodes below. + +## Interface + +Let's examine the interface of a custom node by looking at the bundled example node. + +### Functions + +Every custom node can implement the following methods. + +#### define_schema + +This defines the parameters that the custom node can take as input, and which outputs it produces. + +```python +from comfy_api.latest import io + +@classmethod +def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="ExampleNode", + display_name="Example Node", + category="examples", + inputs=[ + io.Image.Input("image"), + io.String.Input("text", multiline=True, default="Hello"), + ], + outputs=[ + io.Image.Output(), + ], + ) +``` + +Here you can see that inputs and outputs are defined by `io` objects. You can define inputs as `optional=True`, or request [hidden inputs](./more_on_inputs#hidden-inputs). + +Each input has a type (eg. `io.Image`). Widget inputs: `io.Int`, `io.String`, `io.Float`, `io.Boolean` and `io.Combo`, can also have `default`, `min`, `max`, `step` and other options. + + + +`default`: You can define a default value. + +`min`: You can define a min value. + +`max`: You can define a max value. + +`step`: You can use a slider. + + + +#### fingerprint_inputs + +Optional. Allows the node to control when it is re-executed. ComfyUI attempts to only execute nodes that have changed in order to be more efficient. + +#### validate_inputs + +Optional. Allows the node to validate its inputs before execution. + +#### check_lazy_status + +Optional. Allows the node to defer evaluation of lazy inputs. + +### Attributes + +Attributes are properties of the node, and are defined in the `io.Schema` returned by `define_schema`. + +#### outputs + +The types of each output. + +```python +outputs=[ + io.Model.Output(), + io.CLIP.Output(), + io.VAE.Output(), +] +``` + +#### display_name + +Optional: The friendly name of the node shown in the UI. + +```python +display_name="Load Checkpoint", +``` + +#### category + +The category in which the node appears in the UI. + +```python +category="loaders", +``` + +#### Entry Point Function + +The execution method is fixed to `execute` and is a class method: + +```python +@classmethod +def execute(cls, image) -> io.NodeOutput: + # do some processing + return io.NodeOutput(result) +``` + +#### Registering the node + +Instead of dictionaries, custom nodes are registered through a `ComfyExtension` and a module-level `comfy_entrypoint` function: + +```python +from comfy_api.latest import ComfyExtension, io + +class ExampleExtension(ComfyExtension): + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + ExampleNode, + # add more nodes here + ] + +async def comfy_entrypoint() -> ExampleExtension: + return ExampleExtension() +``` + +#### WEB_DIRECTORY + +Custom nodes can have custom UI. + +This attribute sets the web directory. Any `.js` file in that directory will be loaded as a frontend extension. + +Custom nodes can also include markdown documentation in the `WEB_DIRECTORY/docs` folder. See the [Help Page](/custom-nodes/help_page) section for details on how to add rich documentation for your nodes. 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/manager.mdx b/custom-nodes/v3/backend/manager.mdx new file mode 100644 index 000000000..9d460cc81 --- /dev/null +++ b/custom-nodes/v3/backend/manager.mdx @@ -0,0 +1,76 @@ +--- +title: "Publishing to the Manager (V3)" +description: "Understand how to publish a custom node to the ComfyUI Manager database, including git requirements and the custom-node-list.json registration process (V3 schema)." +--- + +{/* +description: "Understand how to publish a custom node to the ComfyUI Manager database." +*/} + + +{/* +## What is a custom node? + +One of the great powers of Comfy is that its node-based approach allows you to develop new workflows by plugging together the nodes provided in different ways. The built-in nodes provide a wide range of functionality, but you may find that you need a feature not provided by a core node. + +Custom nodes are nodes developed by the community. It allows you to implement new features and share them with the wider community. If you are interested in developing custom nodes, you can read more about it [here](/custom-nodes/overview). + +## ComfyUI Manager + +While custom nodes can be installed manually, most people use +[ComfyUI Manager](https://github.com/Comfy-Org/ComfyUI-Manager) to install them. **ComfyUI Manager** takes care of installing, +updating, and removing custom nodes, and any dependencies. But it isn't part +of the Comfy core, so you need to manually install it. + +### Installing ComfyUI Manager + +```bash +cd ComfyUI/custom_nodes +git clone https://github.com/Comfy-Org/ComfyUI-Manager.git +``` + +Restart Comfy afterwards. +See [ComfyUI Manager Install](https://github.com/Comfy-Org/ComfyUI-Manager?tab=readme-ov-file#installation) for details or special cases. + +*/} + +### Using ComfyUI Manager + +To make your custom node available through **ComfyUI Manager** you need to save it as a git repository (generally at `github.com`) +and then submit a Pull Request on the **ComfyUI Manager** git, in which you have edited `custom-node-list.json` to add your node. +[More details](https://github.com/Comfy-Org/ComfyUI-Manager?tab=readme-ov-file#how-to-register-your-custom-node-into-comfyui-manager). + +When a user installs the node, **ComfyUI Manager** will: + + + +git clone the repository, + + +install the pip dependencies listed in the custom node repository under `requirements.txt` (if present), +``` +pip install -r requirements.txt +``` +As is always the case with `pip`, it is possible that your node requirements will be in conflict with other +custom nodes. Don't make your `requirements.txt` any more restrictive than they need to be. + + +execute `install.py`, if it is present in the custom node repository. +`install.py` is executed from the root path of the custom node + + + +### ComfyUI Manager files + +As indicated above, there are a number of files and scripts that **ComfyUI Manager** will use to manage the lifecycle of +a custom node. These are all optional. + +- `requirements.txt` - Python dependencies as mentioned above +- `install.py`, `uninstall.py` - executed when the custom node is installed or uninstalled +Users can just delete the directory, so you can't rely on `uninstall.py` being run +- `disable.py`, `enable.py` - executed when a custom node is disabled or re-enabled +`enable.py` is only run when a disabled node is re-enabled - it should just reverse anything done in `disable.py` +Disabled custom node subdirectory have `.disabled` appended to their names, and Comfy ignores these modules +- `node_list.json` - only required if the custom node's registration pattern is not conventional: the manager detects nodes from a V3 `comfy_entrypoint` / `ComfyExtension` or a legacy V1 `NODE_CLASS_MAPPINGS`, and `node_list.json` spells the mapping out explicitly when neither can be inferred. + +See the [ComfyUI Manager guide](https://github.com/Comfy-Org/ComfyUI-Manager?tab=readme-ov-file#custom-node-support-guide) for official details. 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/custom-nodes/v3/walkthrough.mdx b/custom-nodes/v3/walkthrough.mdx new file mode 100644 index 000000000..411ca299d --- /dev/null +++ b/custom-nodes/v3/walkthrough.mdx @@ -0,0 +1,247 @@ +--- +title: "Getting Started (V3)" +description: "Getting Started with Custom Nodes: a step-by-step ComfyUI walkthrough covering backend node setup, defining a schema, the execute method, registering nodes, adding options, and building a client-side JS extension (V3 schema)." +--- + +This page will take you step-by-step through the process of creating a custom node. + +Our example will take a batch of images, and return one of the images. Initially, the node +will return the image which is, on average, the lightest in color; we'll then extend +it to have a range of selection criteria, and then finally add some client side code. + +This page assumes very little knowledge of Python or Javascript. + +After this walkthrough, dive into the details of [backend code](./backend/server_overview), and +[frontend code](/custom-nodes/js/javascript_overview). + +## Write a basic node + +### Prerequisites + +- A working ComfyUI [installation](/installation/manual_install). For development, we recommend installing ComfyUI manually. +- A working comfy-cli [installation](/comfy-cli/getting-started). + +### Setting up + +```bash +cd ComfyUI/custom_nodes +comfy node scaffold +``` + +After answering a few questions, you'll have a new directory set up. + +```bash + ~ % comfy node scaffold +You've downloaded .cookiecutters/cookiecutter-comfy-extension before. Is it okay to delete and re-download it? [y/n] (y): y + [1/9] full_name (): Comfy + [2/9] email (you@gmail.com): me@comfy.org + [3/9] github_username (your_github_username): comfy + [4/9] project_name (My Custom Nodepack): FirstComfyNode + [5/9] project_slug (firstcomfynode): + [6/9] project_short_description (A collection of custom nodes for ComfyUI): + [7/9] version (0.0.1): + [8/9] Select open_source_license + 1 - GNU General Public License v3 + 2 - MIT license + 3 - BSD license + 4 - ISC license + 5 - Apache Software License 2.0 + 6 - Not open source + Choose from [1/2/3/4/5/6] (1): 1 + [9/9] include_web_directory_for_custom_javascript [y/n] (n): y +Initialized empty Git repository in firstcomfynode/.git/ +✓ Custom node project created successfully! +``` + + +The scaffold generates a legacy V1 example node. In this walkthrough, we'll replace it with the modern V3 schema. See the [V3 Migration guide](/custom-nodes/v3_migration) if you're migrating an existing node. + + +### Defining the node + +Add the following code to the end of `src/nodes.py`: + +```Python src/nodes.py +from comfy_api.latest import ComfyExtension, io + +class ImageSelector(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="ImageSelector", + display_name="Image Selector", + category="example", + inputs=[ + io.Image.Input("images"), + ], + outputs=[ + io.Image.Output(display_name="image"), + ], + ) +``` + +The basic structure of a custom node is described in detail [here](/custom-nodes/v3/backend/server_overview). + +A custom node is defined using a Python class inheriting from `io.ComfyNode`, which must include `define_schema`, a class method defining the node's metadata and inputs/outputs +(see [later](/custom-nodes/v3/backend/server_overview#define_schema) for details of the schema fields), +and an `execute` method that is called when the node is run. + +Notice that the data type for input and output is `io.Image` even though +we expect to receive a batch of images, and return just one. In Comfy, `IMAGE` means +image batch, and a single image is treated as a batch of size 1. + +### The main function + +The main function, `execute`, receives named arguments as defined in the schema, and +returns an `io.NodeOutput`. Since we're dealing with images, which are internally +stored as `torch.Tensor`, + +```Python +import torch +``` + +Then add the function to your class. The datatype for image is `torch.Tensor` with shape `[B,H,W,C]`, +where `B` is the batch size and `C` is the number of channels - 3, for RGB. If we iterate over such +a tensor, we will get a series of `B` tensors of shape `[H,W,C]`. The `.flatten()` method turns +this into a one dimensional tensor, of length `H*W*C`, `torch.mean()` takes the mean, and `.item()` +turns a single value tensor into a Python float. + +```Python + @classmethod + def execute(cls, images) -> io.NodeOutput: + brightness = list(torch.mean(image.flatten()).item() for image in images) + brightest = brightness.index(max(brightness)) + result = images[brightest].unsqueeze(0) + return io.NodeOutput(result) +``` + +Notes on those last two lines: + +- `images[brightest]` will return a Tensor of shape `[H,W,C]`. `unsqueeze` is used to insert a (length 1) dimension at, in this case, dimension zero, to give +us `[B,H,W,C]` with `B=1`: a single image. +- `io.NodeOutput` wraps the result values, matching the order of the schema's `outputs`. + +### Register the node + +To make Comfy recognize the new node, it must be exposed through a `ComfyExtension` and a `comfy_entrypoint` function. Modify the end of `src/nodes.py`: + +```Python src/nodes.py +class ImageSelectorExtension(ComfyExtension): + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + ImageSelector, + ] + +async def comfy_entrypoint() -> ImageSelectorExtension: + return ImageSelectorExtension() +``` + +Then update `__init__.py` to export `comfy_entrypoint` (the scaffold generates a number of V1-related exports, like `NODE_CLASS_MAPPINGS`, which you can remove): + +```Python __init__.py +from .src.firstcomfynode.nodes import comfy_entrypoint + +__all__ = ["comfy_entrypoint"] +``` + +You must restart ComfyUI to see any changes. + +For a detailed explanation of how ComfyUI discovers and loads custom nodes, see the [node lifecycle documentation](/custom-nodes/v3/backend/lifecycle). + +## Add some options + +That node is maybe a bit boring, so we might add some options; a widget that allows you to +choose the brightest image, or the reddest, bluest, or greenest. Edit your schema's `inputs` to look like: + +```Python +from comfy_api.latest import io + +@classmethod +def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="ImageSelector", + display_name="Image Selector", + category="example", + inputs=[ + io.Image.Input("images"), + io.Combo.Input("mode", options=["brightest", "reddest", "greenest", "bluest"]), + ], + outputs=[ + io.Image.Output(display_name="image"), + ], + ) +``` + +Then update the main function. We'll use a fairly naive definition of 'reddest' as being the average +`R` value of the pixels divided by the average of all three colors. So: + +```Python + @classmethod + def execute(cls, images, mode) -> io.NodeOutput: + batch_size = images.shape[0] + brightness = list(torch.mean(image.flatten()).item() for image in images) + if (mode=="brightest"): + scores = brightness + else: + channel = 0 if mode=="reddest" else (1 if mode=="greenest" else 2) + absolute = list(torch.mean(image[:,:,channel].flatten()).item() for image in images) + scores = list( absolute[i]/(brightness[i]+1e-8) for i in range(batch_size) ) + best = scores.index(max(scores)) + result = images[best].unsqueeze(0) + return io.NodeOutput(result) +``` + +## Tweak the UI + +Maybe we'd like a bit of visual feedback, so let's send a little text message to be displayed. + +### Send a message from server + +This requires two lines to be added to the Python code: + +```Python +from server import PromptServer +``` + +Then, inside the body of `execute` (after computing the result), send the message and return the output: + +```Python + PromptServer.instance.send_sync("example.imageselector.textmessage", {"message":f"Picked image {best+1}"}) + return io.NodeOutput(result) +``` + +### Write a client extension + +To add some Javascript to the client, create a subdirectory, `web/js` in your custom node directory, and modify the end of `__init__.py` +to tell Comfy about it by exporting `WEB_DIRECTORY`: + +```Python +WEB_DIRECTORY = "./web/js" +__all__ = ["comfy_entrypoint", "WEB_DIRECTORY"] +``` + +The client extension is saved as a `.js` file in the `web/js` subdirectory, so create `image_selector/web/js/imageSelector.js` with the +code below. (For more, see [client side coding](/custom-nodes/js/javascript_overview)). + +```Javascript +import { app } from "../../scripts/app.js"; +app.registerExtension({ + name: "example.imageselector", + async setup() { + function messageHandler(event) { alert(event.detail.message); } + app.api.addEventListener("example.imageselector.textmessage", messageHandler); + }, +}) +``` + +All we've done is register an extension and add a listener for the message type we are sending in the `setup()` method. This reads the dictionary we sent (which is stored in `event.detail`). + +Stop the Comfy server, start it again, reload the webpage, and run your workflow. + +### The complete example + +The complete example is available at the end of the bundled [`custom_nodes/example_node.py.example`](https://github.com/comfyanonymous/ComfyUI/blob/master/custom_nodes/example_node.py.example). You can download the example workflow [JSON file](https://github.com/Comfy-Org/docs/blob/main/public/workflow.json) or view it below: + +
+ Image Selector Workflow +
diff --git a/docs.json b/docs.json index b6a227405..4df489f9b 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,20 @@ "development/comfyui-server/execution_model_inversion_guide" ] }, + { + "group": "Python (Backend) · V3", + "pages": [ + "custom-nodes/v3/walkthrough", + "custom-nodes/v3/backend/server_overview", + "custom-nodes/v3/backend/interface", + "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", + "custom-nodes/v3/backend/manager" + ] + }, { "group": "JavaScript (UI)", "pages": [ @@ -6656,7 +6670,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 +9975,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 +13193,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 +14378,4 @@ "destination": "/tutorials/partner-nodes/wan/wan3-0" } ] -} +} \ No newline at end of file From 1e3436e5553e36d1753508d6d6685c7ed6210d1a Mon Sep 17 00:00:00 2001 From: "Daxiong (Lin)" Date: Wed, 9 Sep 2026 15:37:32 +0800 Subject: [PATCH 2/2] docs(custom-nodes): keep generic pages out of the V3 route Only schema-reference pages belong in the versioned route. Move generic pages back to their common location: - Remove custom-nodes/v3/walkthrough.mdx (Getting Started tutorial stays single, no version suffix, V1 copy untouched) - Remove custom-nodes/v3/backend/{interface,manager}.mdx (concept + publish pages are schema-agnostic; not V3-specific) - V3 nav group now 6 pages: server_overview, datatypes, more_on_inputs, lazy_evaluation, lifecycle, lists - Entry links (intro/overview/index) point Getting Started back to the common /custom-nodes/walkthrough; Python (Backend) cards point at V3 route --- custom-nodes/intro.mdx | 2 +- custom-nodes/overview.mdx | 4 +- custom-nodes/v3/backend/interface.mdx | 138 -------------- custom-nodes/v3/backend/manager.mdx | 76 -------- custom-nodes/v3/walkthrough.mdx | 247 -------------------------- docs.json | 5 +- 6 files changed, 4 insertions(+), 468 deletions(-) delete mode 100644 custom-nodes/v3/backend/interface.mdx delete mode 100644 custom-nodes/v3/backend/manager.mdx delete mode 100644 custom-nodes/v3/walkthrough.mdx 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/interface.mdx b/custom-nodes/v3/backend/interface.mdx deleted file mode 100644 index 0683caa43..000000000 --- a/custom-nodes/v3/backend/interface.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: "Code Interface (V3)" -description: "What are custom nodes and how are they used? (V3 schema)." ---- - -## Overview - -A custom node is a combination of Python code and potentially some models weights. Custom nodes are extremely powerful, and allows the Comfy community to build their own functionality into ComfyUI. - -If you prefer to read code, check out the [example](https://github.com/Comfy-Org/ComfyUI/blob/master/custom_nodes/example_node.py.example) in the repository. Otherwise, we will explore one of Comfy's built-in nodes below. - -## Interface - -Let's examine the interface of a custom node by looking at the bundled example node. - -### Functions - -Every custom node can implement the following methods. - -#### define_schema - -This defines the parameters that the custom node can take as input, and which outputs it produces. - -```python -from comfy_api.latest import io - -@classmethod -def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="ExampleNode", - display_name="Example Node", - category="examples", - inputs=[ - io.Image.Input("image"), - io.String.Input("text", multiline=True, default="Hello"), - ], - outputs=[ - io.Image.Output(), - ], - ) -``` - -Here you can see that inputs and outputs are defined by `io` objects. You can define inputs as `optional=True`, or request [hidden inputs](./more_on_inputs#hidden-inputs). - -Each input has a type (eg. `io.Image`). Widget inputs: `io.Int`, `io.String`, `io.Float`, `io.Boolean` and `io.Combo`, can also have `default`, `min`, `max`, `step` and other options. - - - -`default`: You can define a default value. - -`min`: You can define a min value. - -`max`: You can define a max value. - -`step`: You can use a slider. - - - -#### fingerprint_inputs - -Optional. Allows the node to control when it is re-executed. ComfyUI attempts to only execute nodes that have changed in order to be more efficient. - -#### validate_inputs - -Optional. Allows the node to validate its inputs before execution. - -#### check_lazy_status - -Optional. Allows the node to defer evaluation of lazy inputs. - -### Attributes - -Attributes are properties of the node, and are defined in the `io.Schema` returned by `define_schema`. - -#### outputs - -The types of each output. - -```python -outputs=[ - io.Model.Output(), - io.CLIP.Output(), - io.VAE.Output(), -] -``` - -#### display_name - -Optional: The friendly name of the node shown in the UI. - -```python -display_name="Load Checkpoint", -``` - -#### category - -The category in which the node appears in the UI. - -```python -category="loaders", -``` - -#### Entry Point Function - -The execution method is fixed to `execute` and is a class method: - -```python -@classmethod -def execute(cls, image) -> io.NodeOutput: - # do some processing - return io.NodeOutput(result) -``` - -#### Registering the node - -Instead of dictionaries, custom nodes are registered through a `ComfyExtension` and a module-level `comfy_entrypoint` function: - -```python -from comfy_api.latest import ComfyExtension, io - -class ExampleExtension(ComfyExtension): - async def get_node_list(self) -> list[type[io.ComfyNode]]: - return [ - ExampleNode, - # add more nodes here - ] - -async def comfy_entrypoint() -> ExampleExtension: - return ExampleExtension() -``` - -#### WEB_DIRECTORY - -Custom nodes can have custom UI. - -This attribute sets the web directory. Any `.js` file in that directory will be loaded as a frontend extension. - -Custom nodes can also include markdown documentation in the `WEB_DIRECTORY/docs` folder. See the [Help Page](/custom-nodes/help_page) section for details on how to add rich documentation for your nodes. diff --git a/custom-nodes/v3/backend/manager.mdx b/custom-nodes/v3/backend/manager.mdx deleted file mode 100644 index 9d460cc81..000000000 --- a/custom-nodes/v3/backend/manager.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: "Publishing to the Manager (V3)" -description: "Understand how to publish a custom node to the ComfyUI Manager database, including git requirements and the custom-node-list.json registration process (V3 schema)." ---- - -{/* -description: "Understand how to publish a custom node to the ComfyUI Manager database." -*/} - - -{/* -## What is a custom node? - -One of the great powers of Comfy is that its node-based approach allows you to develop new workflows by plugging together the nodes provided in different ways. The built-in nodes provide a wide range of functionality, but you may find that you need a feature not provided by a core node. - -Custom nodes are nodes developed by the community. It allows you to implement new features and share them with the wider community. If you are interested in developing custom nodes, you can read more about it [here](/custom-nodes/overview). - -## ComfyUI Manager - -While custom nodes can be installed manually, most people use -[ComfyUI Manager](https://github.com/Comfy-Org/ComfyUI-Manager) to install them. **ComfyUI Manager** takes care of installing, -updating, and removing custom nodes, and any dependencies. But it isn't part -of the Comfy core, so you need to manually install it. - -### Installing ComfyUI Manager - -```bash -cd ComfyUI/custom_nodes -git clone https://github.com/Comfy-Org/ComfyUI-Manager.git -``` - -Restart Comfy afterwards. -See [ComfyUI Manager Install](https://github.com/Comfy-Org/ComfyUI-Manager?tab=readme-ov-file#installation) for details or special cases. - -*/} - -### Using ComfyUI Manager - -To make your custom node available through **ComfyUI Manager** you need to save it as a git repository (generally at `github.com`) -and then submit a Pull Request on the **ComfyUI Manager** git, in which you have edited `custom-node-list.json` to add your node. -[More details](https://github.com/Comfy-Org/ComfyUI-Manager?tab=readme-ov-file#how-to-register-your-custom-node-into-comfyui-manager). - -When a user installs the node, **ComfyUI Manager** will: - - - -git clone the repository, - - -install the pip dependencies listed in the custom node repository under `requirements.txt` (if present), -``` -pip install -r requirements.txt -``` -As is always the case with `pip`, it is possible that your node requirements will be in conflict with other -custom nodes. Don't make your `requirements.txt` any more restrictive than they need to be. - - -execute `install.py`, if it is present in the custom node repository. -`install.py` is executed from the root path of the custom node - - - -### ComfyUI Manager files - -As indicated above, there are a number of files and scripts that **ComfyUI Manager** will use to manage the lifecycle of -a custom node. These are all optional. - -- `requirements.txt` - Python dependencies as mentioned above -- `install.py`, `uninstall.py` - executed when the custom node is installed or uninstalled -Users can just delete the directory, so you can't rely on `uninstall.py` being run -- `disable.py`, `enable.py` - executed when a custom node is disabled or re-enabled -`enable.py` is only run when a disabled node is re-enabled - it should just reverse anything done in `disable.py` -Disabled custom node subdirectory have `.disabled` appended to their names, and Comfy ignores these modules -- `node_list.json` - only required if the custom node's registration pattern is not conventional: the manager detects nodes from a V3 `comfy_entrypoint` / `ComfyExtension` or a legacy V1 `NODE_CLASS_MAPPINGS`, and `node_list.json` spells the mapping out explicitly when neither can be inferred. - -See the [ComfyUI Manager guide](https://github.com/Comfy-Org/ComfyUI-Manager?tab=readme-ov-file#custom-node-support-guide) for official details. diff --git a/custom-nodes/v3/walkthrough.mdx b/custom-nodes/v3/walkthrough.mdx deleted file mode 100644 index 411ca299d..000000000 --- a/custom-nodes/v3/walkthrough.mdx +++ /dev/null @@ -1,247 +0,0 @@ ---- -title: "Getting Started (V3)" -description: "Getting Started with Custom Nodes: a step-by-step ComfyUI walkthrough covering backend node setup, defining a schema, the execute method, registering nodes, adding options, and building a client-side JS extension (V3 schema)." ---- - -This page will take you step-by-step through the process of creating a custom node. - -Our example will take a batch of images, and return one of the images. Initially, the node -will return the image which is, on average, the lightest in color; we'll then extend -it to have a range of selection criteria, and then finally add some client side code. - -This page assumes very little knowledge of Python or Javascript. - -After this walkthrough, dive into the details of [backend code](./backend/server_overview), and -[frontend code](/custom-nodes/js/javascript_overview). - -## Write a basic node - -### Prerequisites - -- A working ComfyUI [installation](/installation/manual_install). For development, we recommend installing ComfyUI manually. -- A working comfy-cli [installation](/comfy-cli/getting-started). - -### Setting up - -```bash -cd ComfyUI/custom_nodes -comfy node scaffold -``` - -After answering a few questions, you'll have a new directory set up. - -```bash - ~ % comfy node scaffold -You've downloaded .cookiecutters/cookiecutter-comfy-extension before. Is it okay to delete and re-download it? [y/n] (y): y - [1/9] full_name (): Comfy - [2/9] email (you@gmail.com): me@comfy.org - [3/9] github_username (your_github_username): comfy - [4/9] project_name (My Custom Nodepack): FirstComfyNode - [5/9] project_slug (firstcomfynode): - [6/9] project_short_description (A collection of custom nodes for ComfyUI): - [7/9] version (0.0.1): - [8/9] Select open_source_license - 1 - GNU General Public License v3 - 2 - MIT license - 3 - BSD license - 4 - ISC license - 5 - Apache Software License 2.0 - 6 - Not open source - Choose from [1/2/3/4/5/6] (1): 1 - [9/9] include_web_directory_for_custom_javascript [y/n] (n): y -Initialized empty Git repository in firstcomfynode/.git/ -✓ Custom node project created successfully! -``` - - -The scaffold generates a legacy V1 example node. In this walkthrough, we'll replace it with the modern V3 schema. See the [V3 Migration guide](/custom-nodes/v3_migration) if you're migrating an existing node. - - -### Defining the node - -Add the following code to the end of `src/nodes.py`: - -```Python src/nodes.py -from comfy_api.latest import ComfyExtension, io - -class ImageSelector(io.ComfyNode): - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="ImageSelector", - display_name="Image Selector", - category="example", - inputs=[ - io.Image.Input("images"), - ], - outputs=[ - io.Image.Output(display_name="image"), - ], - ) -``` - -The basic structure of a custom node is described in detail [here](/custom-nodes/v3/backend/server_overview). - -A custom node is defined using a Python class inheriting from `io.ComfyNode`, which must include `define_schema`, a class method defining the node's metadata and inputs/outputs -(see [later](/custom-nodes/v3/backend/server_overview#define_schema) for details of the schema fields), -and an `execute` method that is called when the node is run. - -Notice that the data type for input and output is `io.Image` even though -we expect to receive a batch of images, and return just one. In Comfy, `IMAGE` means -image batch, and a single image is treated as a batch of size 1. - -### The main function - -The main function, `execute`, receives named arguments as defined in the schema, and -returns an `io.NodeOutput`. Since we're dealing with images, which are internally -stored as `torch.Tensor`, - -```Python -import torch -``` - -Then add the function to your class. The datatype for image is `torch.Tensor` with shape `[B,H,W,C]`, -where `B` is the batch size and `C` is the number of channels - 3, for RGB. If we iterate over such -a tensor, we will get a series of `B` tensors of shape `[H,W,C]`. The `.flatten()` method turns -this into a one dimensional tensor, of length `H*W*C`, `torch.mean()` takes the mean, and `.item()` -turns a single value tensor into a Python float. - -```Python - @classmethod - def execute(cls, images) -> io.NodeOutput: - brightness = list(torch.mean(image.flatten()).item() for image in images) - brightest = brightness.index(max(brightness)) - result = images[brightest].unsqueeze(0) - return io.NodeOutput(result) -``` - -Notes on those last two lines: - -- `images[brightest]` will return a Tensor of shape `[H,W,C]`. `unsqueeze` is used to insert a (length 1) dimension at, in this case, dimension zero, to give -us `[B,H,W,C]` with `B=1`: a single image. -- `io.NodeOutput` wraps the result values, matching the order of the schema's `outputs`. - -### Register the node - -To make Comfy recognize the new node, it must be exposed through a `ComfyExtension` and a `comfy_entrypoint` function. Modify the end of `src/nodes.py`: - -```Python src/nodes.py -class ImageSelectorExtension(ComfyExtension): - async def get_node_list(self) -> list[type[io.ComfyNode]]: - return [ - ImageSelector, - ] - -async def comfy_entrypoint() -> ImageSelectorExtension: - return ImageSelectorExtension() -``` - -Then update `__init__.py` to export `comfy_entrypoint` (the scaffold generates a number of V1-related exports, like `NODE_CLASS_MAPPINGS`, which you can remove): - -```Python __init__.py -from .src.firstcomfynode.nodes import comfy_entrypoint - -__all__ = ["comfy_entrypoint"] -``` - -You must restart ComfyUI to see any changes. - -For a detailed explanation of how ComfyUI discovers and loads custom nodes, see the [node lifecycle documentation](/custom-nodes/v3/backend/lifecycle). - -## Add some options - -That node is maybe a bit boring, so we might add some options; a widget that allows you to -choose the brightest image, or the reddest, bluest, or greenest. Edit your schema's `inputs` to look like: - -```Python -from comfy_api.latest import io - -@classmethod -def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="ImageSelector", - display_name="Image Selector", - category="example", - inputs=[ - io.Image.Input("images"), - io.Combo.Input("mode", options=["brightest", "reddest", "greenest", "bluest"]), - ], - outputs=[ - io.Image.Output(display_name="image"), - ], - ) -``` - -Then update the main function. We'll use a fairly naive definition of 'reddest' as being the average -`R` value of the pixels divided by the average of all three colors. So: - -```Python - @classmethod - def execute(cls, images, mode) -> io.NodeOutput: - batch_size = images.shape[0] - brightness = list(torch.mean(image.flatten()).item() for image in images) - if (mode=="brightest"): - scores = brightness - else: - channel = 0 if mode=="reddest" else (1 if mode=="greenest" else 2) - absolute = list(torch.mean(image[:,:,channel].flatten()).item() for image in images) - scores = list( absolute[i]/(brightness[i]+1e-8) for i in range(batch_size) ) - best = scores.index(max(scores)) - result = images[best].unsqueeze(0) - return io.NodeOutput(result) -``` - -## Tweak the UI - -Maybe we'd like a bit of visual feedback, so let's send a little text message to be displayed. - -### Send a message from server - -This requires two lines to be added to the Python code: - -```Python -from server import PromptServer -``` - -Then, inside the body of `execute` (after computing the result), send the message and return the output: - -```Python - PromptServer.instance.send_sync("example.imageselector.textmessage", {"message":f"Picked image {best+1}"}) - return io.NodeOutput(result) -``` - -### Write a client extension - -To add some Javascript to the client, create a subdirectory, `web/js` in your custom node directory, and modify the end of `__init__.py` -to tell Comfy about it by exporting `WEB_DIRECTORY`: - -```Python -WEB_DIRECTORY = "./web/js" -__all__ = ["comfy_entrypoint", "WEB_DIRECTORY"] -``` - -The client extension is saved as a `.js` file in the `web/js` subdirectory, so create `image_selector/web/js/imageSelector.js` with the -code below. (For more, see [client side coding](/custom-nodes/js/javascript_overview)). - -```Javascript -import { app } from "../../scripts/app.js"; -app.registerExtension({ - name: "example.imageselector", - async setup() { - function messageHandler(event) { alert(event.detail.message); } - app.api.addEventListener("example.imageselector.textmessage", messageHandler); - }, -}) -``` - -All we've done is register an extension and add a listener for the message type we are sending in the `setup()` method. This reads the dictionary we sent (which is stored in `event.detail`). - -Stop the Comfy server, start it again, reload the webpage, and run your workflow. - -### The complete example - -The complete example is available at the end of the bundled [`custom_nodes/example_node.py.example`](https://github.com/comfyanonymous/ComfyUI/blob/master/custom_nodes/example_node.py.example). You can download the example workflow [JSON file](https://github.com/Comfy-Org/docs/blob/main/public/workflow.json) or view it below: - -
- Image Selector Workflow -
diff --git a/docs.json b/docs.json index 4df489f9b..947363a1e 100644 --- a/docs.json +++ b/docs.json @@ -3442,15 +3442,12 @@ { "group": "Python (Backend) · V3", "pages": [ - "custom-nodes/v3/walkthrough", "custom-nodes/v3/backend/server_overview", - "custom-nodes/v3/backend/interface", "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", - "custom-nodes/v3/backend/manager" + "custom-nodes/v3/backend/lists" ] }, {