Add Hollow Mode to easy multiAngle (#936)
- Global control to enable or disable angle prompts - Added `Hollow Mode` for more intuitive visualization - Removed label quantity limit; now supports unlimited additions - Label addition button will copy parameters from the selected page - Double-clicking any face of the cube quickly switches camera angles for easier operation - 全局控制是否添加角度提示词 - 新增了`镂空模式`,可更直观地展示 - 去除标签限制个数,可添加无数个 - 标签添加按钮将复制选中页的参数 - 双击正方体的每一面可以快速切换摄像机角度,便于操作
This commit is contained in:
@@ -0,0 +1,605 @@
|
||||
---
|
||||
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)
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,530 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,635 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
Submodule ComfyUI-Easy-Use-Frontend updated: fadf19fe36...e3ee9efe9b
@@ -3,7 +3,8 @@
|
||||
"Hotkeys": "快捷键",
|
||||
"Nodes": "节点相关",
|
||||
"NodesMap": "管理节点组",
|
||||
"StylesSelector": "样式选择器"
|
||||
"StylesSelector": "样式选择器",
|
||||
"MultiAngle": "摄影机多角度提示词"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"Util": "工具",
|
||||
|
||||
@@ -71,5 +71,12 @@
|
||||
"Grid": "网格",
|
||||
"List": "列表"
|
||||
}
|
||||
},
|
||||
"EasyUse_MultiAngle_HollowMode": {
|
||||
"name": "启用多角度镂空展示模式",
|
||||
"tooltip": "在多角度节点中启用镂空展示模式,可以更直观地查看相机角度"
|
||||
},
|
||||
"EasyUse_MultiAngle_AddAnglePrompt": {
|
||||
"name": "启用添加多角度提示词"
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -1,15 +1,13 @@
|
||||
import json
|
||||
import os
|
||||
from urllib.request import urlopen
|
||||
|
||||
import folder_paths
|
||||
|
||||
from .. import easyCache
|
||||
from ..config import FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE, RESOURCES_DIR
|
||||
from ..libs.log import log_node_info
|
||||
from ..libs.utils import AlwaysEqualProxy
|
||||
from ..libs.wildcards import WildcardProcessor, get_wildcard_list, process
|
||||
|
||||
from urllib.request import urlopen
|
||||
from comfy_api.latest import io
|
||||
|
||||
|
||||
@@ -666,7 +664,7 @@ class multiAngle(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def execute(cls, multi_angle=None, **kwargs):
|
||||
if multi_angle is None or not isinstance(multi_angle, list):
|
||||
if multi_angle is None:
|
||||
return io.NodeOutput([""])
|
||||
|
||||
if isinstance(multi_angle, str):
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import{G as t,r as e,m as n,t as o,H as i,I as r,J as s,h as l,A as a,K as c,L as u,M as d,D as p,N as h,O as f,i as b,a as m,c as g,y as v,x as y,p as x,E as I,P as k,Q as w,w as O,f as S,z as L,l as $,b as C,s as z,R as E,T,F as _,k as M,d as V,S as P,U as F,V as A}from"./vue-C8DvX6GQ.js";import{c as K,n as D,z as B,A as H,B as j,C as R,D as G,F as U,G as N,H as W,I as q,E as X,b as Y,J as Z,f as J,Z as Q,q as tt,K as et,L as nt,M as ot,N as it,O as rt,P as st,Q as lt,R as at,S as ct,T as ut,w as dt,U as pt,i as ht,V as ft,W as bt,X as mt,Y as gt,_ as vt,$ as yt,a0 as xt,r as It,a1 as kt,a2 as wt,a3 as Ot}from"./primeuix-Zf0UbZPU.js";import{B as St,s as Lt,a as $t,b as Ct,c as zt,U as Et,C as Tt,F as _t,d as Mt,e as Vt,f as Pt,g as Ft,h as At,i as Kt,j as Dt,k as Bt,l as Ht}from"./primevue-B4A7h1ix.js";
|
||||
import{E as t,r as e,m as n,t as o,G as i,H as r,I as s,i as l,z as a,J as c,K as u,L as d,C as p,M as h,N as f,j as b,a as m,c as g,x as v,O as y,p as x,D as I,P as k,Q as w,w as O,f as S,y as L,q as $,b as C,n as z,R as E,T,F as _,k as M,d as V,S as P,U as F,V as A}from"./vue-3zBRwU9X.js";import{c as K,n as D,z as B,A as H,B as j,C as R,D as G,F as U,G as N,H as W,I as q,E as X,b as Y,J as Z,f as J,Z as Q,q as tt,K as et,L as nt,M as ot,N as it,O as rt,P as st,Q as lt,R as at,S as ct,T as ut,w as dt,U as pt,i as ht,V as ft,W as bt,X as mt,Y as gt,_ as vt,$ as yt,a0 as xt,r as It,a1 as kt,a2 as wt,a3 as Ot}from"./primeuix-Zf0UbZPU.js";import{B as St,s as Lt,a as $t,b as Ct,c as zt,U as Et,C as Tt,F as _t,d as Mt,e as Vt,f as Pt,g as Ft,h as At,i as Kt,j as Dt,k as Bt,l as Ht}from"./primevue-CjzBNgZL.js";
|
||||
/*!
|
||||
* pinia v2.2.1
|
||||
* (c) 2024 Eduardo San Martin Morote
|
||||
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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user