fix: bump to v1.3.11, remove accidental files, relax protobuf pin

- Remove node.zip and skills/ accidentally committed in previous push
- Add node.zip, skills/, *.png to .gitignore
- Bump version to 1.3.11 for registry publish (1.3.10 already claimed)
- Update stale protobuf error message in __init__.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gero
2026-03-10 02:58:13 +01:00
co-authored by Claude Opus 4.6
parent 09e1c1a201
commit 9816e30ba7
10 changed files with 10 additions and 1264 deletions
+6
View File
@@ -160,3 +160,9 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Local files
node.zip
skills/
*.png
!icon.png
+1
View File
@@ -164,6 +164,7 @@ When modifying or extending this node:
## Version History
- **v1.3.11** (2026-03-10): Relaxed protobuf pin from ==3.20.3 to >=3.20.3 for DA3/onnx compatibility, removed accidental files from repo
- **v1.3.10** (2026-03-09): Removed phantom WEB_DIRECTORY (no js/ dir exists), cleaned up __all__ exports, version bump for registry
- **v1.3.9** (2026-03-09): Version bump to republish after registry conflict (v1.3.8 was claimed by a reverted commit)
- **v1.3.8** (2026-03-09): Added DA3 v1.1 models (Large-1.1, Giant-1.1, Nested-Giant-Large-1.1), fixed duplicate error message in DA3 loading
+2 -2
View File
@@ -12,7 +12,7 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("DepthEstimation")
# Version info
__version__ = "1.3.10"
__version__ = "1.3.11"
# Node class mappings - will be populated based on dependency checks
NODE_CLASS_MAPPINGS = {}
@@ -136,7 +136,7 @@ else:
def error_message(self):
if "Descriptors cannot be created directly" in str(e):
message = "Protobuf version conflict. Run: pip install protobuf==3.20.3"
message = "Protobuf version conflict. Run: pip install protobuf>=3.20.3"
else:
message = f"Error loading depth estimation: {str(e)}"
return (message,)
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyuidepthestimation"
description = "A robust custom depth estimation node for ComfyUI using Depth-Anything models (V1, V2, and V3/DA3). It integrates depth estimation with configurable post-processing options including blur, median filtering, contrast enhancement, and gamma correction."
version = "1.3.10"
version = "1.3.11"
license = { file = "LICENSE" }
dependencies = [
"transformers>=4.20.0",
@@ -1,93 +0,0 @@
# ComfyUI Node Development Skill
A comprehensive skill for developing ComfyUI custom nodes following production best practices.
## Overview
This skill provides guidance for creating ComfyUI custom nodes, from basic structure to advanced patterns like memory management, batch processing, and model integration.
## Installation
### Local (Repository Only)
```bash
cp -r ComfyUI-node-development-skill ~/.claude/skills/
```
### Global Installation
```bash
ln -s "$(pwd)/ComfyUI-node-development-skill" ~/.claude/skills/comfyui-node-development
```
## Usage
This skill automatically activates when you mention:
- "create ComfyUI node"
- "build custom node"
- "ComfyUI development"
- "node for ComfyUI"
- "custom ComfyUI"
## Quick Start
### Basic Node Template
```python
class MyCustomNode:
"""Description of what this node does."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_name": ("INPUT_TYPE", {"default": value}),
}
}
RETURN_TYPES = ("OUTPUT_TYPE",)
FUNCTION = "execute"
CATEGORY = "custom/category"
def execute(self, input_name):
# Your logic here
return (output,)
```
### Register Your Node
In `__init__.py`:
```python
from .my_module import MyCustomNode
NODE_CLASS_MAPPINGS = {
"MyCustomNode": MyCustomNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MyCustomNode": "My Custom Node",
}
```
## Features
- ✅ Node architecture and class structure
- ✅ Input/output type system
- ✅ Widget configurations
- ✅ Model loading patterns
- ✅ Memory management
- ✅ Batch processing
- ✅ Error handling
- ✅ Testing patterns
## Examples
See the `examples/` directory for working node implementations:
- `basic_image_processor.py` - Simple image manipulation
- `model_loader.py` - Custom model loading
- `batch_processor.py` - Efficient batch operations
## References
- [ComfyUI Documentation](https://docs.comfy.org/)
- [Custom Nodes Guide](https://docs.comfy.org/essentials/custom_nodes/)
- [ComfyUI GitHub](https://github.com/comfyanonymous/ComfyUI)
@@ -1,462 +0,0 @@
---
name: comfyui-node-development
description: "This skill should be used when developing ComfyUI custom nodes, creating new node types, implementing AI model integrations, or extending ComfyUI functionality with Python-based nodes."
category: ai-ml
risk: safe
source: community
tags: "[comfyui, ai, stable-diffusion, nodes, python, pytorch]"
date_added: "2026-03-09"
---
# ComfyUI Node Development
## Purpose
Build production-ready ComfyUI custom nodes following official best practices. This skill covers node architecture, input/output handling, execution patterns, model loading, and UI integration for ComfyUI's node-based AI workflow system.
## When to Use This Skill
This skill should be used when:
- Creating new custom nodes for ComfyUI
- Implementing AI model inference pipelines
- Adding custom preprocessing or postprocessing nodes
- Developing nodes for Stable Diffusion workflows
- Extending ComfyUI with third-party integrations
- Debugging node execution issues
- Optimizing node performance for GPU/CPU execution
## Core Capabilities
1. **Node Architecture** - ComfyUI's node class structure and inheritance patterns
2. **Input/Output Types** - ComfyUI type system (IMAGE, LATENT, CONDITIONING, MODEL, etc.)
3. **Execution Model** - Understanding the graph execution and caching mechanism
4. **Model Management** - Loading, caching, and managing AI models in nodes
5. **UI Integration** - Custom widgets, dropdowns, and node visual customization
6. **Error Handling** - Graceful error reporting and recovery in nodes
7. **Performance Optimization** - Memory management, batching, and GPU optimization
## Node Development Fundamentals
### Basic Node Structure
Every ComfyUI node follows this structure:
```python
class MyCustomNode:
"""Node description that appears in the UI."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_name": ("INPUT_TYPE", {"default": value, "min": 0, "max": 100}),
},
"optional": {
"optional_input": ("INPUT_TYPE", {"default": value}),
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
RETURN_TYPES = ("OUTPUT_TYPE",)
RETURN_NAMES = ("output_name",)
FUNCTION = "execute"
CATEGORY = "custom/category"
def execute(self, input_name, optional_input=None, prompt=None, extra_pnginfo=None):
# Node logic here
return (output,)
```
### Input Types Reference
| Type | Description | Example Widget |
|------|-------------|----------------|
| `IMAGE` | Torch tensor (B, H, W, C) | Image input/output |
| `LATENT` | Latent representation | Latent input/output |
| `MODEL` | Diffusion model object | Model loader output |
| `CLIP` | CLIP model for conditioning | CLIP loader output |
| `CONDITIONING` | Text conditioning data | CLIP text encode output |
| `VAE` | VAE model for encode/decode | VAE loader output |
| `MASK` | Single channel image | Mask input/output |
| `INT` | Integer value | Number widget |
| `FLOAT` | Float value | Float widget |
| `STRING` | Text string | Text input |
| `BOOLEAN` | True/False | Toggle widget |
### Widget Configurations
```python
# Integer with constraints
"steps": ("INT", {"default": 20, "min": 1, "max": 10000, "step": 1})
# Float with slider
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01})
# String with multiline
"prompt": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": True})
# Dropdown/Combo
"mode": (["option1", "option2", "option3"], {"default": "option1"})
# Boolean toggle
"enabled": ("BOOLEAN", {"default": True})
```
## Implementation Patterns
### Image Processing Node
```python
import torch
import numpy as np
from PIL import Image
class ImagePreprocessor:
"""Preprocess images for model input."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"target_width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 64}),
"target_height": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 64}),
"interpolation": (["nearest", "bilinear", "bicubic"], {"default": "bilinear"}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("processed_image",)
FUNCTION = "preprocess"
CATEGORY = "image/preprocessing"
def preprocess(self, image, target_width, target_height, interpolation):
# image is (B, H, W, C) tensor in range [0, 1]
batch_size, height, width, channels = image.shape
# Convert to PIL for resizing
images = []
for i in range(batch_size):
img_np = (image[i].cpu().numpy() * 255).astype(np.uint8)
pil_img = Image.fromarray(img_np)
# Resize
pil_img = pil_img.resize((target_width, target_height),
getattr(Image, interpolation.upper()))
# Convert back to tensor
img_np = np.array(pil_img).astype(np.float32) / 255.0
images.append(torch.from_numpy(img_np))
result = torch.stack(images)
return (result,)
```
### Model Wrapper Node
```python
import comfy.model_management as model_management
import folder_paths
class CustomModelLoader:
"""Load custom models with proper memory management."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_name": (folder_paths.get_filename_list("checkpoints"),),
"device": (["auto", "cpu", "cuda"], {"default": "auto"}),
}
}
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
RETURN_NAMES = ("model", "clip", "vae")
FUNCTION = "load_model"
CATEGORY = "loaders"
def load_model(self, model_name, device):
# Get full path
model_path = folder_paths.get_full_path("checkpoints", model_name)
# Load with ComfyUI's model management
if device == "auto":
device = model_management.get_torch_device()
# Load checkpoint (implementation depends on model type)
# Use comfy.utils.load_checkpoint_guess_config or similar
return (model, clip, vae)
```
### Conditional Execution Node
```python
class ConditionalImageProcessor:
"""Process images conditionally based on inputs."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"enabled": ("BOOLEAN", {"default": True}),
},
"optional": {
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process"
CATEGORY = "image/processing"
def process(self, image, enabled, strength=1.0):
if not enabled:
return (image,) # Pass through
# Apply processing
processed = image * strength
processed = torch.clamp(processed, 0, 1)
return (processed,)
```
## Advanced Patterns
### Node with Side Effects
```python
import json
import os
class SaveMetadata:
"""Save workflow metadata alongside images."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
RETURN_TYPES = ()
OUTPUT_NODE = True # Mark as output node
FUNCTION = "save"
CATEGORY = "output"
def save(self, images, filename_prefix, prompt=None, extra_pnginfo=None):
# Save logic here
full_output_folder = folder_paths.get_output_directory()
for batch_number, image in enumerate(images):
# Save image
# Save metadata JSON
if extra_pnginfo is not None:
metadata = {
"prompt": prompt,
"workflow": extra_pnginfo.get("workflow", {})
}
# Write metadata file
return {} # Empty return for output nodes
```
### Batch Processing Node
```python
class BatchImageProcessor:
"""Process multiple images efficiently."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"operation": (["normalize", "invert", "grayscale"],),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "process_batch"
CATEGORY = "image/batch"
def process_batch(self, images, operation):
# Process entire batch at once (GPU accelerated)
if operation == "normalize":
mean = images.mean(dim=(1, 2, 3), keepdim=True)
std = images.std(dim=(1, 2, 3), keepdim=True)
result = (images - mean) / (std + 1e-8)
elif operation == "invert":
result = 1.0 - images
elif operation == "grayscale":
# RGB to grayscale
weights = torch.tensor([0.299, 0.587, 0.114], device=images.device)
gray = (images * weights.view(1, 1, 1, 3)).sum(dim=-1, keepdim=True)
result = gray.expand(-1, -1, -1, 3)
return (torch.clamp(result, 0, 1),)
```
## Best Practices
### Memory Management
```python
import comfy.model_management as mm
class MemoryEfficientNode:
def process(self, model, latent):
# Get the appropriate device
device = mm.get_torch_device()
# Load to device only when needed
model = model.to(device)
latent = latent.to(device)
# Process
with torch.no_grad():
result = model(latent)
# Clean up if needed
mm.soft_empty_cache()
return (result,)
```
### Error Handling
```python
class RobustNode:
def execute(self, image, factor):
try:
if image is None:
raise ValueError("Input image is None")
if factor <= 0:
raise ValueError(f"Factor must be positive, got {factor}")
result = self._process(image, factor)
return (result,)
except Exception as e:
# Log error for debugging
print(f"[RobustNode] Error: {e}")
# Return input unchanged or raise
raise
```
### Caching Considerations
ComfyUI caches node outputs based on inputs. For nodes with non-deterministic behavior:
```python
class RandomGenerator:
"""Generate random values with proper seed handling."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"width": ("INT", {"default": 512, "min": 64, "max": 2048}),
"height": ("INT", {"default": 512, "min": 64, "max": 2048}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate"
CATEGORY = "generators"
def generate(self, seed, width, height):
# Use seed for reproducibility
torch.manual_seed(seed)
# Generate random noise
noise = torch.randn(1, height, width, 3)
noise = (noise - noise.min()) / (noise.max() - noise.min())
return (noise,)
```
## Node Registration
Nodes must be registered in `__init__.py`:
```python
from .my_node_module import MyCustomNode, AnotherNode
NODE_CLASS_MAPPINGS = {
"MyCustomNode": MyCustomNode,
"AnotherNode": AnotherNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MyCustomNode": "My Custom Node",
"AnotherNode": "Another Node",
}
```
## Testing and Debugging
### Unit Testing Pattern
```python
def test_node():
node = MyCustomNode()
# Create test input
test_image = torch.rand(1, 64, 64, 3)
# Execute
result = node.execute(test_image, factor=1.5)
# Verify
assert result[0].shape == test_image.shape
assert result[0].min() >= 0 and result[0].max() <= 1
```
### Debug Logging
```python
import logging
logger = logging.getLogger(__name__)
class DebuggableNode:
def execute(self, input_tensor):
logger.debug(f"Input shape: {input_tensor.shape}")
logger.debug(f"Input device: {input_tensor.device}")
logger.debug(f"Input range: [{input_tensor.min():.4f}, {input_tensor.max():.4f}]")
result = self.process(input_tensor)
logger.debug(f"Output shape: {result.shape}")
return (result,)
```
## Common Pitfalls
1. **Shape Mismatches**: Always verify tensor shapes match expected inputs/outputs
2. **Device Placement**: Ensure tensors are on the correct device (CPU/CUDA)
3. **Value Ranges**: Images should be [0, 1] range, not [0, 255]
4. **Batch Dimension**: Handle batch dimension (B) properly - nodes may receive batched inputs
5. **Memory Leaks**: Use `torch.no_grad()` for inference; clear caches when done
6. **Type Consistency**: RETURN_TYPES must match actual return values exactly
## References
- **ComfyUI Repository**: https://github.com/comfyanonymous/ComfyUI
- **ComfyUI Custom Nodes Guide**: https://docs.comfy.org/essentials/custom_nodes/
- **ComfyUI Node Examples**: https://github.com/comfyanonymous/ComfyUI/tree/master/custom_nodes
@@ -1,126 +0,0 @@
"""
Example: Basic Image Processor Node
A complete working example of a ComfyUI node that resizes images
with various interpolation methods.
"""
import torch
import numpy as np
from PIL import Image
class ExampleImageResizer:
"""
Resize images to target dimensions with multiple interpolation options.
This demonstrates:
- Basic node structure
- IMAGE input/output types
- Dropdown widget configuration
- PIL-based image processing
- Batch processing support
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {
"tooltip": "Input image tensor (B, H, W, C)"
}),
"width": ("INT", {
"default": 512,
"min": 64,
"max": 8192,
"step": 64,
"tooltip": "Target width in pixels"
}),
"height": ("INT", {
"default": 512,
"min": 64,
"max": 8192,
"step": 64,
"tooltip": "Target height in pixels"
}),
"interpolation": (["nearest", "bilinear", "bicubic", "lanczos"], {
"default": "bilinear",
"tooltip": "Resampling method"
}),
},
"optional": {
"maintain_aspect": ("BOOLEAN", {
"default": False,
"tooltip": "Keep original aspect ratio"
}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("resized_image",)
FUNCTION = "resize"
CATEGORY = "example/image"
# Maps string names to PIL constants
INTERPOLATION_MAP = {
"nearest": Image.NEAREST,
"bilinear": Image.BILINEAR,
"bicubic": Image.BICUBIC,
"lanczos": Image.LANCZOS,
}
def resize(self, image, width, height, interpolation, maintain_aspect=False):
"""
Resize input image(s) to target dimensions.
Args:
image: Tensor of shape (B, H, W, C) with values in [0, 1]
width: Target width
height: Target height
interpolation: Resampling method name
maintain_aspect: Whether to preserve aspect ratio
Returns:
Tuple containing resized image tensor
"""
# Get interpolation method
interp_method = self.INTERPOLATION_MAP.get(interpolation, Image.BILINEAR)
batch_size, orig_h, orig_w, channels = image.shape
# Calculate dimensions if maintaining aspect ratio
if maintain_aspect:
aspect = orig_w / orig_h
if width / height > aspect:
width = int(height * aspect)
else:
height = int(width / aspect)
# Process each image in batch
resized_images = []
for i in range(batch_size):
# Convert tensor to PIL (0-255 range)
img_np = (image[i].cpu().numpy() * 255).astype(np.uint8)
pil_img = Image.fromarray(img_np)
# Resize
pil_img = pil_img.resize((width, height), interp_method)
# Convert back to tensor (0-1 range)
img_np = np.array(pil_img).astype(np.float32) / 255.0
resized_images.append(torch.from_numpy(img_np))
# Stack back into batch
result = torch.stack(resized_images)
return (result,)
# Node registration (would go in __init__.py)
NODE_CLASS_MAPPINGS = {
"ExampleImageResizer": ExampleImageResizer,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ExampleImageResizer": "Example: Image Resizer",
}
@@ -1,495 +0,0 @@
# ComfyUI Node Development - Comprehensive Guide
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [Type System Deep Dive](#type-system-deep-dive)
3. [Execution Model](#execution-model)
4. [Model Management](#model-management)
5. [UI Customization](#ui-customization)
6. [Performance Optimization](#performance-optimization)
7. [Advanced Patterns](#advanced-patterns)
---
## Architecture Overview
### How ComfyUI Works
ComfyUI operates on a node-graph execution model:
1. **Graph Definition**: Users create workflows by connecting nodes
2. **Topological Sort**: Nodes are ordered based on dependencies
3. **Execution**: Each node executes when all inputs are ready
4. **Caching**: Outputs are cached based on input hashes for efficiency
### Node Lifecycle
```
Class Definition → Registration → Instantiation → Execution → Cleanup
```
### File Structure
```
my_custom_nodes/
├── __init__.py # Node registration
├── nodes.py # Node implementations
├── utils.py # Helper functions
├── models/ # Model definitions
└── web/ # JavaScript extensions (optional)
└── my_extension.js
```
---
## Type System Deep Dive
### Core Types
#### IMAGE Type
```python
# IMAGE is a torch.Tensor with shape (B, H, W, C)
# Values are in range [0, 1] (float32)
# Channel order is RGB
# Converting to/from PIL
pil_image = Image.fromarray((tensor[0].numpy() * 255).astype(np.uint8))
tensor = torch.from_numpy(np.array(pil_image).astype(np.float32) / 255.0)
```
#### LATENT Type
```python
# LATENT is a dictionary containing:
# - "samples": torch.Tensor of shape (B, C, H, W)
# - "batch_index": Optional list for partial batch processing
latent = {
"samples": torch.randn(1, 4, 64, 64), # For SD 1.5
"batch_index": [0, 1, 2]
}
```
#### MODEL Type
```python
# MODEL wraps the diffusion model
# Access the underlying model with model.model
# Use model.apply_model(x, t, c) for inference
```
#### CONDITIONING Type
```python
# CONDITIONING is a list of tuples:
# [(conditioning_vector, options_dict), ...]
cond = [
(torch.randn(1, 77, 768), {"pooled_output": torch.randn(1, 768)})
]
```
### Custom Types
You can define custom types for type checking:
```python
# In your __init__.py or a types module
CUSTOM_TYPES = {
"MY_CUSTOM_TYPE": "MY_CUSTOM_TYPE",
}
# Use in node
RETURN_TYPES = ("MY_CUSTOM_TYPE",)
```
---
## Execution Model
### Graph Execution Flow
```python
# Pseudo-code of ComfyUI execution
def execute_graph(graph, inputs):
cache = {}
executed = set()
def execute_node(node_id):
if node_id in executed:
return cache[node_id]
node = graph[node_id]
# Execute dependencies first
input_values = []
for input_id in node.inputs:
input_values.append(execute_node(input_id))
# Execute this node
result = node.function(*input_values)
# Cache and return
cache[node_id] = result
executed.add(node_id)
return result
return execute_node(output_node_id)
```
### Forcing Re-execution
Nodes with random outputs should use unique inputs to bypass cache:
```python
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"seed": ("INT", {"default": 0}),
}
}
# Different seed = different cache key = re-execution
```
---
## Model Management
### Model Loading
```python
import folder_paths
import comfy.utils
def load_checkpoint(self, ckpt_name):
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
out = comfy.utils.load_checkpoint_guess_config(
ckpt_path,
output_vae=True,
output_clip=True,
embedding_directory=folder_paths.get_folder_paths("embeddings"),
)
return out[:3] # (model, clip, vae)
```
### Model Patching
```python
class ModelPatcher:
"""Apply patches to models (LoRA, etc.)"""
def patch_model(self, model, patches):
# Create patched copy
model.patch_model(patches)
return model
def unpatch_model(self, model):
# Remove patches
model.unpatch_model()
```
### Memory Optimization
```python
import comfy.model_management as mm
class EfficientNode:
def process(self, model, latent):
device = mm.get_torch_device()
# Move to device
model = model.to(device)
latent = latent.to(device)
# Process with automatic memory management
with mm.autocast():
result = model(latent)
# Optional: free memory
mm.soft_empty_cache()
return result
```
---
## UI Customization
### JavaScript Extensions
Create `web/my_extension.js`:
```javascript
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "MyCustomExtension",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MyCustomNode") {
// Customize node appearance
nodeType.prototype.onDrawForeground = function(ctx) {
// Custom drawing code
};
}
}
});
```
### Custom Widgets
```python
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
# Color picker (requires JS extension)
"color": ("COLOR", {"default": "#ff0000"}),
# File upload
"file": ("FILE", {"accept": ".png,.jpg"}),
# Range slider
"value": ("FLOAT", {
"default": 0.5,
"min": 0,
"max": 1,
"step": 0.01,
"display": "slider" # Requires JS extension
}),
}
}
```
---
## Performance Optimization
### Batching
```python
def process_batch(self, images):
# Process all at once instead of loop
# Much faster on GPU
mean = images.mean(dim=(1, 2, 3), keepdim=True)
return images - mean
```
### In-Place Operations
```python
def modify_inplace(self, tensor):
# Use in-place operations to save memory
tensor.add_(0.1) # In-place addition
tensor.clamp_(0, 1) # In-place clamp
return tensor
```
### Mixed Precision
```python
import torch
def mixed_precision_process(self, model, x):
with torch.cuda.amp.autocast():
output = model(x)
return output
```
---
## Advanced Patterns
### Dynamic Inputs
```python
class DynamicInputNode:
"""Node with variable number of inputs."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
f"input_{i}": ("IMAGE",) for i in range(10)
}
}
def combine(self, **kwargs):
# Collect all non-None inputs
images = [v for k, v in kwargs.items()
if k.startswith("input_") and v is not None]
return (torch.cat(images, dim=0),)
```
### Node Composition
```python
class ComposedNode:
"""Combine multiple operations in one node."""
def __init__(self):
self.sub_node_1 = SubNode1()
self.sub_node_2 = SubNode2()
def execute(self, input_data):
temp = self.sub_node_1.process(input_data)
result = self.sub_node_2.process(temp)
return (result,)
```
### Async Operations
```python
import asyncio
class AsyncNode:
async def fetch_data(self, url):
# Async HTTP request
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.read()
def execute(self, url):
# Run async in sync context
loop = asyncio.get_event_loop()
data = loop.run_until_complete(self.fetch_data(url))
return (data,)
```
---
## Debugging Techniques
### Visual Debugging
```python
class DebugNode:
"""Print debug information about tensors."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"tensor": ("*",), # Accept any type
"print_stats": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("*",)
FUNCTION = "debug"
def debug(self, tensor, print_stats):
print(f"=== DEBUG NODE ===")
print(f"Type: {type(tensor)}")
if isinstance(tensor, torch.Tensor):
print(f"Shape: {tensor.shape}")
print(f"Dtype: {tensor.dtype}")
print(f"Device: {tensor.device}")
print(f"Min/Max: {tensor.min():.4f} / {tensor.max():.4f}")
print(f"Mean/Std: {tensor.mean():.4f} / {tensor.std():.4f}")
elif isinstance(tensor, dict):
print(f"Keys: {tensor.keys()}")
print(f"==================")
return (tensor,)
```
### Progress Reporting
```python
class ProgressNode:
"""Show progress for long operations."""
def long_operation(self, items):
from comfy.utils import ProgressBar
pbar = ProgressBar(len(items))
results = []
for i, item in enumerate(items):
result = self.process(item)
results.append(result)
pbar.update(1)
return results
```
---
## Testing
### Unit Test Example
```python
import unittest
import torch
class TestMyNode(unittest.TestCase):
def setUp(self):
self.node = MyCustomNode()
def test_basic_execution(self):
input_tensor = torch.rand(1, 64, 64, 3)
result = self.node.execute(input_tensor)
self.assertEqual(result[0].shape, input_tensor.shape)
self.assertTrue(torch.all(result[0] >= 0))
self.assertTrue(torch.all(result[0] <= 1))
def test_batch_processing(self):
batch = torch.rand(4, 64, 64, 3)
result = self.node.execute(batch)
self.assertEqual(result[0].shape[0], 4)
if __name__ == "__main__":
unittest.main()
```
---
## Common Issues and Solutions
### Issue: Shape Mismatch
```python
# Wrong: Assuming fixed batch size
def wrong(self, image):
return image[0] # Loses batch dimension
# Right: Preserve batch dimension
def right(self, image):
return image # Keep (B, H, W, C)
```
### Issue: Device Mismatch
```python
# Wrong: Mixing CPU and CUDA tensors
def wrong(self, tensor1, tensor2):
return tensor1 + tensor2 # May fail if different devices
# Right: Ensure same device
def right(self, tensor1, tensor2):
device = tensor1.device
tensor2 = tensor2.to(device)
return tensor1 + tensor2
```
### Issue: Value Range
```python
# Wrong: PIL expects 0-255, tensor is 0-1
pil_img = Image.fromarray(tensor.numpy())
# Right: Scale appropriately
pil_img = Image.fromarray((tensor.numpy() * 255).astype(np.uint8))
```
@@ -1,85 +0,0 @@
#!/usr/bin/env python3
"""
Validation script for ComfyUI Node Development Skill
"""
import os
import sys
import re
from pathlib import Path
def validate_yaml_frontmatter(content):
"""Validate YAML frontmatter format."""
pattern = r'^---\n(.*?)\n---'
match = re.match(pattern, content, re.DOTALL)
if not match:
return False, "No YAML frontmatter found"
yaml_content = match.group(1)
required_fields = ['name', 'description']
for field in required_fields:
if f'{field}:' not in yaml_content:
return False, f"Missing required field: {field}"
return True, "YAML frontmatter valid"
def validate_word_count(content, max_words=5000):
"""Check word count."""
words = len(content.split())
if words > max_words:
return False, f"Word count too high: {words} (max {max_words})"
return True, f"Word count: {words}"
def validate_skill(skill_path):
"""Validate a skill directory."""
skill_path = Path(skill_path)
print(f"🔍 Validating skill: {skill_path.name}")
print("-" * 50)
# Check required files
required_files = ['SKILL.md', 'README.md']
for file in required_files:
file_path = skill_path / file
if not file_path.exists():
print(f"❌ Missing required file: {file}")
return False
print(f"✅ Found {file}")
# Validate SKILL.md
skill_md = skill_path / 'SKILL.md'
content = skill_md.read_text(encoding='utf-8')
# YAML validation
valid, msg = validate_yaml_frontmatter(content)
if valid:
print(f"✅ {msg}")
else:
print(f"❌ {msg}")
return False
# Word count
valid, msg = validate_word_count(content)
if valid:
print(f"✅ {msg}")
else:
print(f"⚠️ {msg}")
print("-" * 50)
print("✅ Skill validation complete!")
return True
if __name__ == "__main__":
if len(sys.argv) > 1:
skill_path = sys.argv[1]
else:
skill_path = Path(__file__).parent.parent
success = validate_skill(skill_path)
sys.exit(0 if success else 1)