diff --git a/.gitignore b/.gitignore index 82f9275..2144bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index eedd6d8..a55e0ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/__init__.py b/__init__.py index ee16e39..a6ca1fd 100644 --- a/__init__.py +++ b/__init__.py @@ -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,) diff --git a/node.zip b/node.zip deleted file mode 100644 index cdb7ba6..0000000 Binary files a/node.zip and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index 04e9766..f275759 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/skills/ComfyUI-node-development-skill/README.md b/skills/ComfyUI-node-development-skill/README.md deleted file mode 100644 index 48a514f..0000000 --- a/skills/ComfyUI-node-development-skill/README.md +++ /dev/null @@ -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) diff --git a/skills/ComfyUI-node-development-skill/SKILL.md b/skills/ComfyUI-node-development-skill/SKILL.md deleted file mode 100644 index 6a2b17a..0000000 --- a/skills/ComfyUI-node-development-skill/SKILL.md +++ /dev/null @@ -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 diff --git a/skills/ComfyUI-node-development-skill/examples/basic_image_processor.py b/skills/ComfyUI-node-development-skill/examples/basic_image_processor.py deleted file mode 100644 index 82b5011..0000000 --- a/skills/ComfyUI-node-development-skill/examples/basic_image_processor.py +++ /dev/null @@ -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", -} diff --git a/skills/ComfyUI-node-development-skill/references/comprehensive-guide.md b/skills/ComfyUI-node-development-skill/references/comprehensive-guide.md deleted file mode 100644 index 475d724..0000000 --- a/skills/ComfyUI-node-development-skill/references/comprehensive-guide.md +++ /dev/null @@ -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)) -``` diff --git a/skills/ComfyUI-node-development-skill/scripts/validate.py b/skills/ComfyUI-node-development-skill/scripts/validate.py deleted file mode 100755 index 26056f0..0000000 --- a/skills/ComfyUI-node-development-skill/scripts/validate.py +++ /dev/null @@ -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)