Fix NodesMap #969

This commit is contained in:
yolain
2026-03-19 14:26:21 +08:00
parent d60b61d575
commit aef19b8772
9 changed files with 993 additions and 3128 deletions
@@ -1,605 +0,0 @@
---
applyTo: "**/*.py"
description: "ComfyUI v3 Node Examples"
---
# ComfyUI v3 Node Examples
Real-world examples of v3 nodes demonstrating various features and patterns.
## Basic Examples
### Simple Image Processor
```python
from comfy_api.latest import io, ui
import torch
class ImageInvertV3(io.ComfyNode):
"""Simple node that inverts image colors."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageInvert_v3",
display_name="Invert Image",
category="image/filters",
description="Inverts the colors of an image",
inputs=[
io.Image.Input("image", tooltip="Image to invert")
],
outputs=[
io.Image.Output("inverted", tooltip="Inverted image")
]
)
@classmethod
def execute(cls, image):
# Invert: 1.0 - image
inverted = 1.0 - image
return io.NodeOutput(inverted, ui=ui.PreviewImage(inverted))
```
### Math Operations
```python
class MathOperationV3(io.ComfyNode):
"""Performs math operations on two values."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MathOperation_v3",
display_name="Math Operation",
category="utils/math",
inputs=[
io.Float.Input("a", default=0.0),
io.Float.Input("b", default=0.0),
io.Combo.Input("operation",
options=["add", "subtract", "multiply", "divide", "power"],
default="add"
)
],
outputs=[
io.Float.Output("result")
]
)
@classmethod
def execute(cls, a, b, operation):
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b != 0 else 0,
"power": a ** b
}
result = operations[operation]
return io.NodeOutput(result)
```
## Async Examples
### API Integration
```python
import aiohttp
class TextGeneratorV3(io.ComfyNode):
"""Generates text using external API."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="TextGenerator_v3",
display_name="AI Text Generator",
category="text/generation",
inputs=[
io.String.Input("prompt", multiline=True),
io.String.Input("api_url", default="http://localhost:11434/api/generate"),
io.String.Input("model", default="llama2"),
io.Float.Input("temperature", default=0.7, min=0.0, max=2.0)
],
outputs=[
io.String.Output("generated_text")
]
)
@classmethod
async def execute(cls, prompt, api_url, model, temperature):
async with aiohttp.ClientSession() as session:
payload = {
"model": model,
"prompt": prompt,
"temperature": temperature,
"stream": False
}
async with session.post(api_url, json=payload) as response:
if response.status == 200:
data = await response.json()
text = data.get("response", "")
return io.NodeOutput(text)
else:
raise RuntimeError(f"API error: {response.status}")
```
### Batch Processing with Progress
```python
from comfy.utils import ProgressBar
import asyncio
class BatchImageProcessorV3(io.ComfyNode):
"""Processes images in batch with progress tracking."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="BatchImageProcessor_v3",
display_name="Batch Image Processor",
category="image/batch",
inputs=[
io.Image.Input("images"),
io.Float.Input("process_time", default=0.1, min=0.01, max=1.0,
tooltip="Simulated processing time per image")
],
outputs=[
io.Image.Output("processed")
],
hidden=[io.Hidden.unique_id]
)
@classmethod
async def execute(cls, images, process_time, **kwargs):
batch_size = images.shape[0]
pbar = ProgressBar(batch_size, node_id=cls.hidden.unique_id)
processed = []
for i in range(batch_size):
# Simulate async processing
await asyncio.sleep(process_time)
# Example: Apply blur
import torch.nn.functional as F
blurred = F.gaussian_blur(images[i:i+1], kernel_size=5)
processed.append(blurred)
pbar.update(1)
result = torch.cat(processed, dim=0)
return io.NodeOutput(result, ui=ui.PreviewImage(result))
```
## Advanced Examples
### Model Loader with Resources
```python
import folder_paths
import comfy.utils
import comfy.sd
class CheckpointLoaderV3(io.ComfyNode):
"""Loads checkpoint models with caching."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CheckpointLoader_v3",
display_name="Load Checkpoint",
category="loaders",
inputs=[
io.Combo.Input("ckpt_name",
options=folder_paths.get_filename_list("checkpoints"),
tooltip="Select checkpoint to load"
)
],
outputs=[
io.Model.Output("model"),
io.Clip.Output("clip"),
io.Vae.Output("vae")
]
)
@classmethod
def execute(cls, ckpt_name):
# Use resource caching
ckpt = cls.resources.get(
resources.TorchDictFolderFilename("checkpoints", ckpt_name)
)
# Load components
model, clip, vae = comfy.sd.load_checkpoint_guess_config(
ckpt,
embedding_directory=folder_paths.get_folder_paths("embeddings")
)
return io.NodeOutput(model, clip, vae)
```
### State Management Example
```python
class IterativeRefinerV3(io.ComfyNode):
"""Refines images iteratively with state tracking."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="IterativeRefiner_v3",
display_name="Iterative Refiner",
category="image/processing",
inputs=[
io.Image.Input("image"),
io.Int.Input("iterations", default=3, min=1, max=10),
io.Boolean.Input("reset", default=False,
tooltip="Reset refinement history")
],
outputs=[
io.Image.Output("refined"),
io.Int.Output("total_iterations")
]
)
@classmethod
def execute(cls, image, iterations, reset):
# Initialize or reset state
if reset or cls.state.history is None:
cls.state.history = []
cls.state.total_iterations = 0
# Get last refined image or use input
current = cls.state.history[-1] if cls.state.history else image
# Iterative refinement
for i in range(iterations):
# Example: Progressive sharpening
import torch.nn.functional as F
kernel = torch.tensor([[-1,-1,-1],
[-1, 9,-1],
[-1,-1,-1]], dtype=torch.float32)
kernel = kernel.view(1, 1, 3, 3)
kernel = kernel.repeat(current.shape[-1], 1, 1, 1)
current = current.permute(0, 3, 1, 2)
sharpened = F.conv2d(current, kernel, padding=1, groups=current.shape[1])
current = sharpened.permute(0, 2, 3, 1)
current = torch.clamp(current, 0, 1)
# Update state
cls.state.history.append(current)
cls.state.total_iterations += iterations
# Keep history size manageable
if len(cls.state.history) > 10:
cls.state.history.pop(0)
return io.NodeOutput(
current,
cls.state.total_iterations,
ui=ui.PreviewImage(current)
)
```
### Dynamic Inputs Example
```python
class ImageBlenderV3(io.ComfyNode):
"""Blends multiple images with weights."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageBlender_v3",
display_name="Image Blender",
category="image/blend",
inputs=[
io.AutoGrowDynamicInput("images",
template_input=io.Image.Input("image"),
min=2,
max=8
),
io.Combo.Input("mode",
options=["average", "weighted", "max", "min"],
default="average"
)
],
outputs=[
io.Image.Output("blended")
]
)
@classmethod
def execute(cls, mode, **kwargs):
# Collect all image inputs
images = []
for key, value in sorted(kwargs.items()):
if key.startswith("image"):
images.append(value)
if not images:
raise ValueError("No images provided")
# Stack images
stacked = torch.stack(images, dim=0)
# Blend based on mode
if mode == "average":
blended = torch.mean(stacked, dim=0)
elif mode == "weighted":
# Simple linear weighting
weights = torch.linspace(1, 0.1, len(images))
weights = weights / weights.sum()
weights = weights.view(-1, 1, 1, 1, 1)
blended = (stacked * weights).sum(dim=0)
elif mode == "max":
blended = torch.max(stacked, dim=0)[0]
elif mode == "min":
blended = torch.min(stacked, dim=0)[0]
return io.NodeOutput(blended, ui=ui.PreviewImage(blended))
```
### Multi-Type Input Example
```python
class UniversalInverterV3(io.ComfyNode):
"""Inverts images, masks, or conditioning."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="UniversalInverter_v3",
display_name="Universal Inverter",
category="utils/invert",
inputs=[
io.MultiType.Input("input",
types=[io.Image, io.Mask, io.Conditioning]
),
io.Float.Input("strength", default=1.0, min=0.0, max=1.0)
],
outputs=[
io.MultiType.Output("inverted",
types=[io.Image, io.Mask, io.Conditioning]
)
]
)
@classmethod
def execute(cls, input, strength):
# Detect input type and process accordingly
if isinstance(input, torch.Tensor):
# Image or Mask
if input.dim() == 4: # Image [B,H,W,C]
inverted = 1.0 - input
inverted = input + (inverted - input) * strength
return io.NodeOutput(inverted, ui=ui.PreviewImage(inverted))
else: # Mask [H,W] or [B,H,W]
inverted = 1.0 - input
inverted = input + (inverted - input) * strength
return io.NodeOutput(inverted, ui=ui.PreviewMask(inverted))
elif isinstance(input, list): # Conditioning
# Invert conditioning strength
inverted = []
for cond, data in input:
new_data = data.copy()
if 'strength' in new_data:
new_data['strength'] = 1.0 - new_data['strength']
inverted.append((cond, new_data))
return io.NodeOutput(inverted)
else:
raise ValueError(f"Unsupported input type: {type(input)}")
```
### Custom Type Example
```python
class CustomDataProcessorV3(io.ComfyNode):
"""Processes custom data types."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CustomDataProcessor_v3",
display_name="Custom Data Processor",
category="utils/custom",
inputs=[
io.Custom(io_type="MY_CUSTOM_TYPE").Input("custom_data",,
tooltip="Custom data type input"
),
io.Float.Input("scale", default=1.0, min=0.1, max=10.0)
],
outputs=[
io.Custom(io_type="MY_CUSTOM_TYPE").Output("processed_data",
tooltip="Processed custom data"
)
]
)
@classmethod
def execute(cls, custom_data, scale):
# Process custom data type
# Assuming custom_data is a dict with 'value' and 'metadata'
processed = {
'value': custom_data.get('value', 0) * scale,
'metadata': custom_data.get('metadata', {}),
'processed': True
}
return io.NodeOutput(processed)
```
## Process Isolation Example
### Node with Specific Dependencies
```python
# manifest.yaml
"""
name: scientific_processor
version: 1.0.0
dependencies:
- numpy==1.24.0 # Specific older version needed
- scipy==1.10.0
- scikit-image==0.20.0
isolated: true
share_torch: true
"""
# __init__.py
from comfy_api.latest import io, io.ComfyNode, io.Schema
import numpy as np
from skimage import filters
class ScientificProcessorV3(io.ComfyNode):
"""Image processing with scientific libraries."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ScientificProcessor_v3",
display_name="Scientific Processor",
category="image/scientific",
inputs=[
io.Image.Input("image"),
io.Combo.Input("filter_type",
options=["gaussian", "sobel", "laplacian", "butterworth"],
default="gaussian"
),
io.Float.Input("sigma", default=1.0, min=0.1, max=10.0)
],
outputs=[
io.Image.Output("filtered")
]
)
@classmethod
def execute(cls, image, filter_type, sigma):
# Convert to numpy
img_np = image.cpu().numpy()
batch_size = img_np.shape[0]
results = []
for i in range(batch_size):
img = img_np[i]
if filter_type == "gaussian":
filtered = filters.gaussian(img, sigma=sigma, channel_axis=-1)
elif filter_type == "sobel":
gray = np.mean(img, axis=-1)
filtered = filters.sobel(gray)
filtered = np.stack([filtered]*3, axis=-1)
elif filter_type == "laplacian":
gray = np.mean(img, axis=-1)
filtered = filters.laplace(gray)
filtered = np.stack([filtered]*3, axis=-1)
elif filter_type == "butterworth":
# Frequency domain filtering
for c in range(3):
channel = img[:,:,c]
fft = np.fft.fft2(channel)
fft_shift = np.fft.fftshift(fft)
# Apply Butterworth filter
H = 1 / (1 + (D/sigma)**4) # Simplified
filtered_fft = fft_shift * H
filtered[:,:,c] = np.real(np.fft.ifft2(np.fft.ifftshift(filtered_fft)))
results.append(filtered)
# Convert back to tensor
result = torch.from_numpy(np.stack(results)).float()
return io.NodeOutput(result, ui=ui.PreviewImage(result))
# Entry point for pyisolate
from pyisolate import ExtensionBase
class ScientificExtension(ExtensionBase):
def on_module_loaded(self, module):
self.nodes = {
"ScientificProcessor_v3": ScientificProcessorV3
}
def create_extension():
return ScientificExtension()
```
## Complete Workflow Example
```python
class TextToImageWorkflowV3(io.ComfyNode):
"""Complete text-to-image workflow in one node."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="TextToImageWorkflow_v3",
display_name="Text to Image Workflow",
category="workflows",
description="All-in-one text to image generation",
inputs=[
io.String.Input("positive_prompt", multiline=True),
io.String.Input("negative_prompt", multiline=True, default=""),
io.Model.Input("model"),
io.Clip.Input("clip"),
io.Vae.Input("vae"),
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("steps", default=20, min=1, max=150),
io.Float.Input("cfg", default=7.0, min=0.0, max=30.0),
io.Combo.Input("sampler_name",
options=comfy.samplers.KSampler.SAMPLERS,
default="euler"
),
io.Combo.Input("scheduler",
options=comfy.samplers.KSampler.SCHEDULERS,
default="normal"
),
io.Int.Input("width", default=1024, min=64, max=8192, step=8),
io.Int.Input("height", default=1024, min=64, max=8192, step=8),
io.Int.Input("batch_size", default=1, min=1, max=64)
],
outputs=[
io.Image.Output("images", is_output_list=True),
io.Latent.Output("latents")
],
is_output_node=True
)
@classmethod
async def execute(cls, positive_prompt, negative_prompt, model, clip, vae,
seed, steps, cfg, sampler_name, scheduler,
width, height, batch_size):
import comfy.samplers
# Encode prompts
positive_cond = clip.encode_from_text(positive_prompt)
negative_cond = clip.encode_from_text(negative_prompt)
# Create empty latent
latent = torch.zeros([batch_size, 4, height // 8, width // 8])
# Set up sampler
sampler = comfy.samplers.KSampler(
model, steps, cfg, sampler_name, scheduler,
positive_cond, negative_cond, latent,
denoise=1.0, seed=seed
)
# Sample with progress callback
def callback(step, x0, x, total_steps):
# Could update progress here
pass
samples = sampler.sample(latent, callback=callback)
# Decode latents
images = vae.decode(samples["samples"])
return io.NodeOutput(
images,
samples,
ui=ui.PreviewImage(images)
)
```
@@ -1,530 +0,0 @@
---
applyTo: "**/*.py"
description: "ComfyUI v3 Migration Guide"
---
# ComfyUI v3 Migration Guide
This guide helps developers migrate existing v1 nodes to the new v3 schema and take advantage of async execution and process isolation.
## Quick Start: The Core Changes
1. **Inherit from `io.ComfyNode`**: Your node class now subclasses `io.ComfyNode`.
2. **Use `define_schema`**: All metadata (`INPUT_TYPES`, `CATEGORY`, etc.) moves into a single `@classmethod def define_schema(cls)` that returns an `io.Schema` object.
3. **Use `execute`**: The main logic function is now always a `@classmethod def execute(cls, ...)` method.
4. **Use Typed I/O**: Inputs and outputs are now strongly-typed objects from the `io` module (e.g., `io.Image.Input(...)`).
5. **Return `NodeOutput`**: The `execute` method must return an `io.NodeOutput` instance.
6. **Use `NODES_LIST`**: Node registration is done by adding the class to a `NODES_LIST` at the end of the file, replacing `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS`.
## Step-by-Step Migration
### Step 1: Class Definition and Schema
**V1:**
```python
class Canny:
CATEGORY = "image/preprocessors"
FUNCTION = "detect_edge"
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE",),
"low_threshold": ("FLOAT", {"default": 0.4}),
"high_threshold": ("FLOAT", {"default": 0.8}),
}}
def detect_edge(self, image, low_threshold, high_threshold):
# ... logic ...
return (img_out,)
NODE_CLASS_MAPPINGS = {"Canny": Canny}
```
**V3:**
```python
from comfy_api.latest import io
class Canny(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Canny_V3",
category="image/preprocessors",
inputs=[
io.Image.Input("image"),
io.Float.Input("low_threshold", default=0.4),
io.Float.Input("high_threshold", default=0.8),
],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, image, low_threshold, high_threshold):
# ... logic ...
return io.NodeOutput(img_out)
NODES_LIST = [Canny]
```
### Step 2: Naming and Registration (`node_id`, `display_name`, `NODES_LIST`)
This is a critical step for ensuring your V3 node coexists with or replaces the V1 version correctly.
1. **Remove Old Mappings**: Delete the `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` dictionaries.
2. **Create `NODES_LIST`**: Create a new list called `NODES_LIST` and add your V3 class to it.
3. **Set `node_id`**: The `node_id` in `Schema` **must** be the key from the old `NODE_CLASS_MAPPINGS`.
4. **Set `display_name` (Conditionally)**:
- Check if a key existed in the old `NODE_DISPLAY_NAME_MAPPINGS`.
- **If yes**: Set `display_name` to that value.
- **If no**: **Omit** the `display_name` parameter from `Schema` entirely.
**Example:**
**V1 Registration:**
```python
NODE_CLASS_MAPPINGS = {
"APG": APG,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"APG": "Adaptive Projected Guidance",
}
```
**V3 `define_schema`:**
```python
@classmethod
def define_schema(cls):
return io.Schema(
node_id="APG_V3", # From MAPPINGS key + "_V3"
display_name="Adaptive Projected Guidance _V3", # From DISPLAY MAPPINGS value + " _V3"
# ... other parameters
)
NODES_LIST = [APG] # ... at end of file
```
### Step 3: Converting I/O
| V1 Type (`string`) | V3 Class (`io.<Type>`) | Common `Input()` Options (as keyword arguments) |
|:-----------------------|:------------------------|:--------------------------------------------------------------------------|
| `STRING` | `io.String` | `default`, `multiline`, `dynamic_prompts`, `placeholder` |
| `INT` | `io.Int` | `default`, `min`, `max`, `step`, `display_mode`, `control_after_generate` |
| `FLOAT` | `io.Float` | `default`, `min`, `max`, `step`, `round`, `display_mode` |
| `BOOLEAN` | `io.Boolean` | `default`, `label_on`, `label_off` |
| `COMBO` | `io.Combo` | `options`, `default`, `upload`, `image_folder`, `remote` |
| (custom) | `io.MultiCombo` | `options`, `default`, `placeholder`, `chip` |
| `IMAGE` | `io.Image` | |
| `MASK` | `io.Mask` | |
| `MESH` | `io.Mesh` | |
| `HOOKS` | `io.Hooks` | |
| `HOOK_KEYFRAMES` | `io.HookKeyframes` | |
| `LATENT` | `io.Latent` | |
| `LATENT_OPERATION` | `io.LatentOperation` | |
| `LOAD3D_CAMERA` | `io.Load3DCamera` | |
| `LOAD_3D` | `io.Load3D` | |
| `LOAD_3D_ANIMATION` | `io.Load3DAnimation` | |
| `LOSS_MAP` | `io.LossMap` | |
| `LORA_MODEL` | `io.LoraModel` | |
| `CONDITIONING` | `io.Conditioning` | |
| `CLIP` | `io.Clip` | |
| `CLIP_VISION_OUTPUT` | `io.ClipVisionOutput` | |
| `NOISE` | `io.Noise` | |
| `VAE` | `io.Vae` | |
| `MODEL` | `io.Model` | |
| `CONTROL_NET` | `io.ControlNet` | |
| `SAMPLER` | `io.Sampler` | |
| `SIGMAS` | `io.Sigmas` | |
| `GUIDER` | `io.Guider` | |
| `CLIP_VISION` | `io.ClipVision` | |
| `UPSCALE_MODEL` | `io.UpscaleModel` | |
| `AUDIO` | `io.Audio` | |
| `VIDEO` | `io.Video` | |
| `VOXEL` | `io.Voxel` | |
| `WAN_CAMERA_EMBEDDING` | `io.WanCameraEmbedding` | |
| `WEBCAM` | `io.Webcam` | `default`, `socketless` |
| `*` | `io.AnyType` | Used for inputs that can accept any type, like the PreviewAny node. |
#### Advanced Input Types
**MultiType Input (accepts multiple types):**
```python
io.MultiType.Input("input", types=[io.Mask, io.Float, io.Int], optional=True)
```
**Combo with Remote Options:**
```python
io.Combo.Input(
"lora_name",
options=folder_paths.get_filename_list("loras"),
tooltip="The name of the LoRA."
)
```
**Optional Parameters:**
```python
io.Boolean.Input(
"case_sensitive",
default=True,
optional=True, # Makes this input optional
tooltip="Whether to use case-sensitive matching"
)
```
### Step 4: Migrating Logic
- **Execution Method**: Rename your old `FUNCTION` to `execute` and make it a `@classmethod`.
- **Return Value**: Wrap your return tuple in `io.NodeOutput()`. For UI updates, use the `ui` keyword argument: `io.NodeOutput(ui=ui.PreviewImage(image))`.
- **State**: Replace `self.variable` with `cls.state.variable`.
- **Hidden Inputs**: Replace `prompt` and `unique_id` parameters with `cls.hidden.prompt` and `cls.hidden.unique_id`. Request them in the schema with `hidden=[io.Hidden.prompt, io.Hidden.unique_id]`.
- **Optional Methods**: `IS_CHANGED` becomes `fingerprint_inputs`, and `VALIDATE_INPUTS` becomes `validate_inputs`. Both should be `@classmethod`.
## Common Migration Patterns
### 1. Hidden Inputs
**V1:**
```python
"hidden": {
"prompt": "PROMPT",
"unique_id": "UNIQUE_ID"
}
def execute(self, ..., prompt=None, unique_id=None):
... # Use hidden inputs
```
**V3:**
```python
hidden=[
io.Hidden.prompt,
io.Hidden.unique_id
]
@classmethod
def execute(cls, ...):
# Access via **cls**
prompt = cls.hidden.prompt
unique_id = cls.hidden.unique_id
```
### 2. State Management
**V1:**
```python
def __init__(self):
self.last_seed = None
self.cache = {}
def execute(self, seed, ...):
if seed != self.last_seed:
self.cache.clear()
self.last_seed = seed
```
**V3:**
```python
@classmethod
def execute(cls, seed, ...):
if cls.state.last_seed != seed:
cls.state.cache = {}
cls.state.last_seed = seed
```
### 3. UI Output
**V1:**
```python
def execute(self, image):
# Save preview manually
preview = save_temp_image(image)
return {"ui": {"images": preview}, "result": (image,)}
```
**V3:**
```python
@classmethod
def execute(cls, image):
return io.NodeOutput(image, ui=ui.PreviewImage(image))
```
### 4. Dynamic Inputs
**V1:**
```python
@classmethod
def INPUT_TYPES(s):
# Complex logic to generate dynamic inputs
inputs = {"required": {}}
for i in range(get_dynamic_count()):
inputs["required"][f"input_{i}"] = ("IMAGE",)
return inputs
```
**V3:**
```python
inputs=[
io.AutoGrowDynamic.Input("images",
template_input=io.Image.Input("image"),
min=1,
max=10
)
]
```
### 5. Resource Loading
**V1:**
```python
def execute(self, model_name):
# Direct file loading
model_path = folder_paths.get_full_path("checkpoints", model_name)
model = comfy.utils.load_torch_file(model_path)
```
**V3:**
```python
from comfy_api.latest import resources
@classmethod
def execute(cls, model_name):
# Cached resource loading
model = cls.resources.get(
resources.TorchDictFolderFilename("checkpoints", model_name)
)
```
## Making Nodes Async
### Basic Async Node
```python
class AsyncNodeV3(io.ComfyNode):
@classmethod
async def execute(cls, image, url):
# Network request without blocking
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
data = await response.json()
# Process with the data
result = process_image_with_data(image, data)
return io.NodeOutput(result)
```
### Progress Tracking
```python
@classmethod
async def execute(cls, images, unique_id):
from comfy.utils import ProgressBar
batch_size = images.shape[0]
pbar = ProgressBar(batch_size, node_id=unique_id)
results = []
for i in range(batch_size):
# Async processing
result = await process_single(images[i])
results.append(result)
pbar.update(1)
return io.NodeOutput(torch.cat(results))
```
## Enabling Process Isolation
### 1. Create manifest.yaml
```yaml
name: my_custom_nodes
version: 1.0.0
description: My custom node collection
author: Your Name
dependencies:
- numpy==1.26.4
- scikit-image>=0.22.0
- opencv-python
isolated: true
share_torch: true
```
### 2. Update __init__.py
```python
from pyisolate import ExtensionBase
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
class MyNodesExtension(ExtensionBase):
def on_module_loaded(self, module):
# Nodes are automatically registered
pass
async def get_node_mappings(self):
return NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
# Extension entry point
def create_extension():
return MyNodesExtension()
```
## Practical Migration Examples
### Complete String Node Conversion
This example shows a full conversion of the StringConcatenate node from v1 to v3:
**V1 Implementation:**
```python
class StringConcatenate():
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"string_a": (IO.STRING, {"multiline": True}),
"string_b": (IO.STRING, {"multiline": True}),
"delimiter": (IO.STRING, {"multiline": False, "default": ""})
}
}
RETURN_TYPES = (IO.STRING,)
FUNCTION = "execute"
CATEGORY = "utils/string"
def execute(self, string_a, string_b, delimiter, **kwargs):
return delimiter.join((string_a, string_b)),
```
**V3 Implementation:**
```python
from comfy_api.latest import io, ui
class StringConcatenate(io.ComfyNode):
"""Concatenates two strings with an optional delimiter between them."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringConcatenate",
display_name="String Concatenate",
category="utils/string",
description="Concatenates two strings together with an optional delimiter between them.",
inputs=[
io.String.Input(
"string_a",
display_name="String A",
multiline=True,
tooltip="The first string to concatenate"
),
io.String.Input(
"string_b",
display_name="String B",
multiline=True,
tooltip="The second string to concatenate"
),
io.String.Input(
"delimiter",
display_name="Delimiter",
default="",
multiline=False,
tooltip="The delimiter to insert between the two strings (empty by default)"
),
],
outputs=[
io.String.Output(
"concatenated",
display_name="Concatenated String",
tooltip="The result of concatenating string_a and string_b with the delimiter"
),
],
)
@classmethod
def execute(cls, string_a: str, string_b: str, delimiter: str) -> io.NodeOutput:
"""Concatenates two strings with an optional delimiter."""
result = delimiter.join((string_a, string_b))
return io.NodeOutput(result)
```
### Replacing V1 Nodes Strategy
When replacing v1 nodes with v3 implementations:
1. **Keep Original Node Names**: Don't add "V3" suffix to maintain compatibility
2. **Preserve All Parameters**: Keep same parameter names and defaults
3. **Maintain Return Structure**: v3 automatically generates v1-compatible returns
4. **Test Workflow Compatibility**: Ensure existing workflows continue to work
Example migration workflow:
```bash
# 1. Create new branch
git checkout -b v3-node-migration
# 2. Backup original
cp nodes_original.py nodes_original.py.bak
# 3. Replace with v3 version
cp nodes_v3.py nodes_original.py
# 4. Test with existing workflows
comfy-cli test-workflows ./test-workflows/
```
## Testing Your Migration
### 1. Backward Compatibility Test
```python
# Your v3 node should work with v1 calls
def test_v1_compatibility():
node = MyNodeV3()
inputs = node.INPUT_TYPES()
assert "required" in inputs
assert hasattr(node, "FUNCTION")
assert hasattr(node, "RETURN_TYPES")
```
### 2. Async Execution Test
```python
import asyncio
async def test_async_execution():
result = await MyAsyncNode.execute(image=test_image)
assert result is not None
```
### 3. Isolation Test
```bash
# Test with conflicting dependencies
comfy-cli test-node --isolated my_custom_nodes
```
## Best Practices
1. **Keep nodes stateless** - Use `cls.state` for any mutable data
2. **Make I/O operations async** - Network, disk, database operations
3. **Use resource caching** - Via `cls.resources.get()`
4. **Declare all dependencies** - In manifest.yaml
5. **Test both sync and async** - Ensure compatibility
6. **Document type changes** - Help users update workflows
## Common Issues
### Issue: State not persisting
**Solution:** Use `cls.state` instead of instance variables
### Issue: Hidden inputs not working
**Solution:** Access via `cls.hidden.unique_id` not function parameters
### Issue: Async not executing
**Solution:** Ensure method is `async def` and use `await` for async calls
### Issue: Import errors in isolation
**Solution:** Add all dependencies to manifest.yaml
### Issue: Tensors not sharing
**Solution:** Enable `share_torch: true` in manifest.yaml
@@ -1,635 +0,0 @@
---
applyTo: "**/*.py"
description: "ComfyUI v3 API Reference"
---
# ComfyUI v3 API Reference
Complete reference for the ComfyUI v3 node API, including all types, methods, and decorators.
## Core Classes
### ComfyNodeV3
Base class for all v3 nodes.
```python
from comfy_api.latest import io
class CustomNode(io.ComfyNode):
# Class properties set during execution
state: NodeState # Persistent state storage
resources: Resources # Resource loader with caching
hidden: HiddenHolder # Access to hidden inputs
@classmethod
@abstractmethod
def define_schema(cls) -> io.ComfyNode:
"""Define node schema. Must be overridden."""
pass
@classmethod
@abstractmethod
def execute(cls, **kwargs) -> io.NodeOutput:
"""Execute node logic. Can be async."""
pass
@classmethod
def validate_inputs(cls, **kwargs) -> bool:
"""Optional: Validate inputs before execution."""
pass
@classmethod
def fingerprint_inputs(cls, **kwargs) -> Any:
"""Optional: Generate a fingerprint for caching."""
pass
@classmethod
def GET_SERIALIZERS(cls) -> list[Serializer]:
"""Optional: Define custom serializers."""
return []
```
### io.ComfyNode
Node definition schema.
```python
@dataclass
class io.ComfyNode:
node_id: str # Globally unique ID
display_name: str = None # UI display name
category: str = "sd" # Node category
inputs: list[InputV3] = None # Input definitions
outputs: list[OutputV3] = None # Output definitions
hidden: list[Hidden] = None # Hidden inputs
description: str = "" # Tooltip description
is_input_list: bool = False # Handle list inputs
is_output_node: bool = False # Force execution
is_deprecated: bool = False # Mark as deprecated
is_experimental: bool = False # Mark as experimental
is_api_node: bool = False # API node flag
not_idempotent: bool = False # Disable caching
```
### NodeOutput
Structured return value from `execute`.
```python
class NodeOutput:
def __init__(
self,
*args: Any, # Output values
ui: UIOutput | dict = None, # UI elements
expand: dict = None, # Subgraph expansion
block_execution: str = None # Execution blocker
):
pass
```
## Input Types
### Basic Inputs
```python
# Integer input
io.Int.Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: int = None,
min: int = None,
max: int = None,
step: int = None,
control_after_generate: bool = None,
display_mode: NumberDisplay = None,
socketless: bool = None,
force_input: bool = None
)
# Float input
io.Float.Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: float = None,
min: float = None,
max: float = None,
step: float = None,
round: float = None,
display_mode: NumberDisplay = None,
socketless: bool = None,
force_input: bool = None
)
# String input
io.String.Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
multiline: bool = False,
placeholder: str = None,
default: str = None,
dynamic_prompts: bool = None,
socketless: bool = None,
force_input: bool = None
)
# Boolean input
io.Boolean.Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: bool = None,
label_on: str = None,
label_off: str = None,
socketless: bool = None,
force_input: bool = None
)
# Combo (dropdown) input
io.Combo.Input(
id: str,
options: list[str] = None,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: str = None,
control_after_generate: bool = None,
upload: UploadType = None,
image_folder: FolderType = None,
remote: RemoteOptions = None,
socketless: bool = None
)
# Multi-select combo
io.MultiCombo.Input(
id: str,
options: list[str],
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: list[str] = None,
placeholder: str = None,
chip: bool = None,
control_after_generate: bool = None,
socketless: bool = None
)
# cusotm type
io.Custom(io_type="MY_TYPE").Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
placeholder: str = None,
)
```
### ComfyUI Types
```python
# Core types
io.Image.Input(id, ...) # Type: torch.Tensor [B,H,W,C]
io.Mask.Input(id, ...) # Type: torch.Tensor [H,W] or [B,H,W]
io.Latent.Input(id, ...) # Type: dict with 'samples' tensor
io.Conditioning.Input(id, ...) # Type: list[tuple[tensor, dict]]
io.Model.Input(id, ...) # Type: ModelPatcher
io.Clip.Input(id, ...) # Type: CLIP
io.Vae.Input(id, ...) # Type: VAE
io.ControlNet.Input(id, ...) # Type: ControlNet
# Sampling types
io.Sampler.Input(id, ...) # Type: Sampler
io.Sigmas.Input(id, ...) # Type: torch.Tensor
io.Noise.Input(id, ...) # Type: torch.Tensor
io.Guider.Input(id, ...) # Type: CFGGuider
# Additional types
io.ClipVision.Input(id, ...) # Type: ClipVisionModel
io.ClipVisionOutput.Input(id, ...) # Type: ClipVisionOutput
io.StyleModel.Input(id, ...) # Type: StyleModel
io.Gligen.Input(id, ...) # Type: ModelPatcher
io.UpscaleModel.Input(id, ...) # Type: ImageModelDescriptor
io.Audio.Input(id, ...) # Type: dict with 'waveform' and 'sample_rate'
io.Video.Input(id, ...) # Type: VideoInput
io.Webcam.Input(id, ...) # Type: str (filepath)
io.WanCameraEmbedding.Input(id, ...) # Type: torch.Tensor
io.LoraModel.Input(id, ...) # Type: dict[str, Tensor]
io.Hooks.Input(id, ...) # Type: HookGroup
io.HookKeyframes.Input(id, ...) # Type: HookKeyframeGroup
io.SVG.Input(id, ...) # Type: SVG (custom class)
io.Voxel.Input(id, ...) # Type: Voxel data (custom class)
io.Mesh.Input(id, ...) # Type: Mesh data (custom class)
```
### Advanced Inputs
```python
# Multi-type input (accepts multiple types)
io.MultiType.Input(
id: str | InputV3, # Can override from existing input
types: list[type[ComfyType]],
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None
)
# Dynamic growing input
io.AutogrowDynamic.Input(
id: str,
template_input: InputV3, # Template for each new input
min: int = 1, # Minimum inputs
max: int = None # Maximum inputs
)
# Custom type
@io.comfytype(io_type="MY_CUSTOM")
class MyCustom:
Type = MyDataClass
class Input(io.InputV3):
...
class Output(io.OutputV3):
...
```
## Output Types
```python
# Basic output
io.Image.Output(
id: str = None,
display_name: str = None,
tooltip: str = None,
is_output_list: bool = False # Output is list
)
# All ComfyUI types have corresponding outputs
io.Mask.Output(id, ...)
io.Latent.Output(id, ...)
io.Model.Output(id, ...)
io.Clip.Output(id, ...)
io.Vae.Output(id, ...)
io.Conditioning.Output(id, ...)
io.String.Output(id, ...)
io.Int.Output(id, ...)
io.Float.Output(id, ...)
io.Boolean.Output(id, ...)
# ... etc
```
## Hidden Inputs
```python
from comfy_api.latest import Hidden
# Available hidden inputs
Hidden.unique_id # Node's unique ID
Hidden.prompt # Complete prompt
Hidden.extra_pnginfo # PNG metadata dict
Hidden.dynprompt # Dynamic prompt object
Hidden.auth_token_comfy_org # ComfyOrg auth token
Hidden.api_key_comfy_org # ComfyOrg API key
# Usage in schema
hidden=[
Hidden.unique_id,
Hidden.prompt
]
# Access in execute
unique_id = cls.hidden.unique_id
prompt = cls.hidden.prompt
```
## State Management
```python
# NodeState interface
class NodeState:
def get_value(self, key: str) -> Any
def set_value(self, key: str, value: Any)
def pop(self, key: str) -> Any
def __contains__(self, key: str) -> bool
# Attribute access
cls.state.my_value = 42
value = cls.state.my_value
# Dictionary access
cls.state["key"] = "value"
value = cls.state["key"]
```
## Practical Input/Output Examples
### Enhanced Documentation with Tooltips and Display Names
```python
# String input with full documentation
io.String.Input(
"prompt",
display_name="Text Prompt",
multiline=True,
default="A beautiful landscape",
tooltip="Enter the text description for image generation",
placeholder="Type your prompt here..."
)
# Integer with constraints and UI hints
io.Int.Input(
"steps",
display_name="Sampling Steps",
default=20,
min=1,
max=150,
tooltip="Number of denoising steps. Higher values take longer but may produce better results",
display_mode=io.NumberDisplay.slider
)
# Combo with dynamic options
io.Combo.Input(
"checkpoint",
options=folder_paths.get_filename_list("checkpoints"),
display_name="Model Checkpoint",
tooltip="Select the AI model to use for generation"
)
# Output with documentation
io.Image.Output(
"generated_image",
display_name="Generated Image",
tooltip="The final generated image based on your prompt"
)
# Combo with dynamic options and file upload
io.Combo.Input(
"audio_file",
options=sorted(folder_paths.filter_files_content_types(os.listdir(folder_paths.get_input_directory()), ["audio", "video"])),
display_name="Audio File",
tooltip="Select an audio file or upload a new one",
upload=io.UploadType.audio
)
```
### Return Pattern with NodeOutput
```python
@classmethod
def execute(cls, text: str, count: int) -> io.NodeOutput:
# Single output
result = process_text(text, count)
return io.NodeOutput(result)
# Multiple outputs
image, mask = generate_image_and_mask(text)
return io.NodeOutput(image, mask)
# With UI preview
image = generate_image(text)
return io.NodeOutput(image, ui=ui.PreviewImage(image))
# With multiple UI elements
images = batch_generate(text, count)
previews = [ui.PreviewImage(img) for img in images]
return io.NodeOutput(images, ui={"images": previews})
```
## Resource Management
```python
# Load cached resources
from comfy_api.latest import resources
# Load torch file
model = cls.resources.get(
resources.TorchDictFolderFilename(
folder_name="checkpoints", # Folder category
file_name="model.safetensors"
)
)
# With default value
model = cls.resources.get(key, default=None)
# Custom resource types (future)
class MyResourceKey(ResourceKey):
Type = MyResourceType
def __init__(self, ...):
pass
```
## UI Output Classes
```python
from comfy_api.latest import ui
# Image preview
ui.PreviewImage(
image: torch.Tensor,
animated: bool = False
)
# Mask preview
ui.PreviewMask(
mask: torch.Tensor,
animated: bool = False
)
# Audio preview
ui.PreviewAudio(
values: list[SavedResult | dict]
)
# Text output
ui.PreviewText(
value: str
)
# 3D preview
ui.PreviewUI3D(
values: list[SavedResult | dict]
)
```
## Decorators and Helpers
```python
# Create custom ComfyType
@io.comfytype(io_type="CUSTOM_TYPE")
class CustomType:
Type = CustomClass
class Input(io.InputV3):
...
class Output(io.OutputV3):
...
# Custom serializer
class MySerializer(Serializer, io_type="MY_TYPE"):
@classmethod
def serialize(cls, obj: Any) -> str:
return json.dumps(obj)
@classmethod
def deserialize(cls, s: str) -> Any:
return json.loads(s)
```
## Async Support
```python
# Async execute
class AsyncNode(io.ComfyNode):
@classmethod
async def execute(cls, **kwargs):
result = await async_operation()
return io.NodeOutput(result)
# Async validation
@classmethod
async def VALIDATE_INPUTS(cls, **kwargs):
is_valid = await check_validity()
return True if is_valid else "Error message"
# Async lazy check
async def check_lazy_status(cls, **kwargs):
needed = await determine_needed_inputs()
return needed # List of input names
```
## Complete Example
```python
from comfy_api.latest import io, ui, resources, io.ComfyNode, io.ComfyNode
import torch
class AdvancedNodeV3(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.ComfyNode(
node_id="AdvancedNode",
display_name="Advanced Node",
category="examples/advanced",
description="Demonstrates v3 features",
inputs=[
# Basic inputs
io.Image.Input("image", tooltip="Input image"),
io.Model.Input("model", tooltip="Model to use"),
# Configured inputs
io.Float.Input("strength",
default=0.75,
min=0.0,
max=1.0,
step=0.05,
display_mode=io.NumberDisplay.slider
),
# Multi-type
io.MultiType.Input("flexible",
types=[io.Image, io.Mask, io.Latent],
optional=True
),
# Dynamic
io.AutoGrowDynamic.Input("extra_images",
template_input=io.Image.Input("img"),
min=0,
max=5
)
],
outputs=[
io.Image.Output("result", tooltip="Processed image"),
io.Latent.Output("latent", is_output_list=True)
],
hidden=[
io.Hidden.unique_id,
io.Hidden.prompt
],
is_output_node=True,
is_experimental=True
)
@classmethod
async def execute(cls, image, model, strength, flexible=None, **kwargs):
# Access state
if cls.state.last_model != model:
cls.state.cache = {}
cls.state.last_model = model
# Load resources
weights = cls.resources.get(
resources.TorchDictFolderFilename("loras", "style.safetensors"),
default=None
)
# Access hidden
node_id = cls.hidden.unique_id
# Process async
result = await process_with_model(image, model, strength)
# Handle dynamic inputs
extra_images = [v for k, v in kwargs.items() if k.startswith("extra_")]
# Return with UI
return io.NodeOutput(
result,
[latent],
ui=ui.PreviewImage(result)
)
@classmethod
async def fingerprint_inputs(cls, strength, **kwargs):
if strength < 0.1:
return "Strength too low for good results"
return True
```
## Type Reference
### Type Mappings
| v3 Type | Python Type | Shape/Format |
|---------|------------|--------------|
| `io.Image.Type` | `torch.Tensor` | `[B,H,W,C]` float32 0-1 |
| `io.Mask.Type` | `torch.Tensor` | `[H,W]` or `[B,H,W]` float32 |
| `io.Latent.Type` | `dict` | `{"samples": tensor, ...}` |
| `io.Conditioning.Type` | `list` | `[(tensor, dict), ...]` |
| `io.Audio.Type` | `dict` | `{"waveform": tensor, "sample_rate": int}` |
| `io.Int.Type` | `int` | Python integer |
| `io.Float.Type` | `float` | Python float |
| `io.String.Type` | `str` | Python string |
| `io.Boolean.Type` | `bool` | Python boolean |
### Enum Types
```python
# Number display modes
io.NumberDisplay.number # Standard input
io.NumberDisplay.slider # Slider widget
io.NumberDisplay.color # Color picker widget
# Folder types
io.FolderType.input # Input folder
io.FolderType.output # Output folder
io.FolderType.temp # Temp folder
# Upload types
io.UploadType.image
io.UploadType.audio
io.UploadType.video
io.UploadType.model
```
+1
View File
@@ -12,6 +12,7 @@ web_version/dev/**
docs/**
.vscode/
.idea/
.claude/**
mmb-preset.custom.txt
config.yaml
node.tar.gz
+989 -1355
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long