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:
yolain
2026-01-11 15:51:14 +08:00
committed by GitHub
parent e94405e610
commit d9c2072a2d
15 changed files with 1789 additions and 13 deletions
@@ -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
```
+2 -1
View File
@@ -3,7 +3,8 @@
"Hotkeys": "快捷键",
"Nodes": "节点相关",
"NodesMap": "管理节点组",
"StylesSelector": "样式选择器"
"StylesSelector": "样式选择器",
"MultiAngle": "摄影机多角度提示词"
},
"nodeCategories": {
"Util": "工具",
+7
View File
@@ -71,5 +71,12 @@
"Grid": "网格",
"List": "列表"
}
},
"EasyUse_MultiAngle_HollowMode": {
"name": "启用多角度镂空展示模式",
"tooltip": "在多角度节点中启用镂空展示模式,可以更直观地查看相机角度"
},
"EasyUse_MultiAngle_AddAnglePrompt": {
"name": "启用添加多角度提示词"
}
}
+2 -4
View File
@@ -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
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