Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5e31ef12a | ||
|
|
21b3c15040 | ||
|
|
5dfcbcf51d | ||
|
|
070001b36b | ||
|
|
6b4c89adc4 | ||
|
|
32ad26f0e1 | ||
|
|
d9c2072a2d | ||
|
|
e94405e610 | ||
|
|
03d5a4cf12 | ||
|
|
ad43ed3154 | ||
|
|
5cc1f8535a | ||
|
|
9f42ead9db | ||
|
|
23d9c365bd | ||
|
|
7a17ad010d | ||
|
|
3b38a5ae60 | ||
|
|
3b224fbccd |
@@ -0,0 +1,605 @@
|
||||
---
|
||||
applyTo: "**/*.py"
|
||||
description: "ComfyUI v3 Node Examples"
|
||||
---
|
||||
|
||||
# ComfyUI v3 Node Examples
|
||||
|
||||
Real-world examples of v3 nodes demonstrating various features and patterns.
|
||||
|
||||
## Basic Examples
|
||||
|
||||
### Simple Image Processor
|
||||
|
||||
```python
|
||||
from comfy_api.latest import io, ui
|
||||
import torch
|
||||
|
||||
class ImageInvertV3(io.ComfyNode):
|
||||
"""Simple node that inverts image colors."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="ImageInvert_v3",
|
||||
display_name="Invert Image",
|
||||
category="image/filters",
|
||||
description="Inverts the colors of an image",
|
||||
inputs=[
|
||||
io.Image.Input("image", tooltip="Image to invert")
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output("inverted", tooltip="Inverted image")
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, image):
|
||||
# Invert: 1.0 - image
|
||||
inverted = 1.0 - image
|
||||
return io.NodeOutput(inverted, ui=ui.PreviewImage(inverted))
|
||||
```
|
||||
|
||||
### Math Operations
|
||||
|
||||
```python
|
||||
class MathOperationV3(io.ComfyNode):
|
||||
"""Performs math operations on two values."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="MathOperation_v3",
|
||||
display_name="Math Operation",
|
||||
category="utils/math",
|
||||
inputs=[
|
||||
io.Float.Input("a", default=0.0),
|
||||
io.Float.Input("b", default=0.0),
|
||||
io.Combo.Input("operation",
|
||||
options=["add", "subtract", "multiply", "divide", "power"],
|
||||
default="add"
|
||||
)
|
||||
],
|
||||
outputs=[
|
||||
io.Float.Output("result")
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, a, b, operation):
|
||||
operations = {
|
||||
"add": a + b,
|
||||
"subtract": a - b,
|
||||
"multiply": a * b,
|
||||
"divide": a / b if b != 0 else 0,
|
||||
"power": a ** b
|
||||
}
|
||||
result = operations[operation]
|
||||
return io.NodeOutput(result)
|
||||
```
|
||||
|
||||
## Async Examples
|
||||
|
||||
### API Integration
|
||||
|
||||
```python
|
||||
import aiohttp
|
||||
|
||||
class TextGeneratorV3(io.ComfyNode):
|
||||
"""Generates text using external API."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="TextGenerator_v3",
|
||||
display_name="AI Text Generator",
|
||||
category="text/generation",
|
||||
inputs=[
|
||||
io.String.Input("prompt", multiline=True),
|
||||
io.String.Input("api_url", default="http://localhost:11434/api/generate"),
|
||||
io.String.Input("model", default="llama2"),
|
||||
io.Float.Input("temperature", default=0.7, min=0.0, max=2.0)
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("generated_text")
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, prompt, api_url, model, temperature):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"temperature": temperature,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
async with session.post(api_url, json=payload) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
text = data.get("response", "")
|
||||
return io.NodeOutput(text)
|
||||
else:
|
||||
raise RuntimeError(f"API error: {response.status}")
|
||||
```
|
||||
|
||||
### Batch Processing with Progress
|
||||
|
||||
```python
|
||||
from comfy.utils import ProgressBar
|
||||
import asyncio
|
||||
|
||||
class BatchImageProcessorV3(io.ComfyNode):
|
||||
"""Processes images in batch with progress tracking."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="BatchImageProcessor_v3",
|
||||
display_name="Batch Image Processor",
|
||||
category="image/batch",
|
||||
inputs=[
|
||||
io.Image.Input("images"),
|
||||
io.Float.Input("process_time", default=0.1, min=0.01, max=1.0,
|
||||
tooltip="Simulated processing time per image")
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output("processed")
|
||||
],
|
||||
hidden=[io.Hidden.unique_id]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, images, process_time, **kwargs):
|
||||
batch_size = images.shape[0]
|
||||
pbar = ProgressBar(batch_size, node_id=cls.hidden.unique_id)
|
||||
|
||||
processed = []
|
||||
for i in range(batch_size):
|
||||
# Simulate async processing
|
||||
await asyncio.sleep(process_time)
|
||||
|
||||
# Example: Apply blur
|
||||
import torch.nn.functional as F
|
||||
blurred = F.gaussian_blur(images[i:i+1], kernel_size=5)
|
||||
processed.append(blurred)
|
||||
|
||||
pbar.update(1)
|
||||
|
||||
result = torch.cat(processed, dim=0)
|
||||
return io.NodeOutput(result, ui=ui.PreviewImage(result))
|
||||
```
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
### Model Loader with Resources
|
||||
|
||||
```python
|
||||
import folder_paths
|
||||
import comfy.utils
|
||||
import comfy.sd
|
||||
|
||||
class CheckpointLoaderV3(io.ComfyNode):
|
||||
"""Loads checkpoint models with caching."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="CheckpointLoader_v3",
|
||||
display_name="Load Checkpoint",
|
||||
category="loaders",
|
||||
inputs=[
|
||||
io.Combo.Input("ckpt_name",
|
||||
options=folder_paths.get_filename_list("checkpoints"),
|
||||
tooltip="Select checkpoint to load"
|
||||
)
|
||||
],
|
||||
outputs=[
|
||||
io.Model.Output("model"),
|
||||
io.Clip.Output("clip"),
|
||||
io.Vae.Output("vae")
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, ckpt_name):
|
||||
# Use resource caching
|
||||
ckpt = cls.resources.get(
|
||||
resources.TorchDictFolderFilename("checkpoints", ckpt_name)
|
||||
)
|
||||
|
||||
# Load components
|
||||
model, clip, vae = comfy.sd.load_checkpoint_guess_config(
|
||||
ckpt,
|
||||
embedding_directory=folder_paths.get_folder_paths("embeddings")
|
||||
)
|
||||
|
||||
return io.NodeOutput(model, clip, vae)
|
||||
```
|
||||
|
||||
### State Management Example
|
||||
|
||||
```python
|
||||
class IterativeRefinerV3(io.ComfyNode):
|
||||
"""Refines images iteratively with state tracking."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="IterativeRefiner_v3",
|
||||
display_name="Iterative Refiner",
|
||||
category="image/processing",
|
||||
inputs=[
|
||||
io.Image.Input("image"),
|
||||
io.Int.Input("iterations", default=3, min=1, max=10),
|
||||
io.Boolean.Input("reset", default=False,
|
||||
tooltip="Reset refinement history")
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output("refined"),
|
||||
io.Int.Output("total_iterations")
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, image, iterations, reset):
|
||||
# Initialize or reset state
|
||||
if reset or cls.state.history is None:
|
||||
cls.state.history = []
|
||||
cls.state.total_iterations = 0
|
||||
|
||||
# Get last refined image or use input
|
||||
current = cls.state.history[-1] if cls.state.history else image
|
||||
|
||||
# Iterative refinement
|
||||
for i in range(iterations):
|
||||
# Example: Progressive sharpening
|
||||
import torch.nn.functional as F
|
||||
kernel = torch.tensor([[-1,-1,-1],
|
||||
[-1, 9,-1],
|
||||
[-1,-1,-1]], dtype=torch.float32)
|
||||
kernel = kernel.view(1, 1, 3, 3)
|
||||
kernel = kernel.repeat(current.shape[-1], 1, 1, 1)
|
||||
|
||||
current = current.permute(0, 3, 1, 2)
|
||||
sharpened = F.conv2d(current, kernel, padding=1, groups=current.shape[1])
|
||||
current = sharpened.permute(0, 2, 3, 1)
|
||||
current = torch.clamp(current, 0, 1)
|
||||
|
||||
# Update state
|
||||
cls.state.history.append(current)
|
||||
cls.state.total_iterations += iterations
|
||||
|
||||
# Keep history size manageable
|
||||
if len(cls.state.history) > 10:
|
||||
cls.state.history.pop(0)
|
||||
|
||||
return io.NodeOutput(
|
||||
current,
|
||||
cls.state.total_iterations,
|
||||
ui=ui.PreviewImage(current)
|
||||
)
|
||||
```
|
||||
|
||||
### Dynamic Inputs Example
|
||||
|
||||
```python
|
||||
class ImageBlenderV3(io.ComfyNode):
|
||||
"""Blends multiple images with weights."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="ImageBlender_v3",
|
||||
display_name="Image Blender",
|
||||
category="image/blend",
|
||||
inputs=[
|
||||
io.AutoGrowDynamicInput("images",
|
||||
template_input=io.Image.Input("image"),
|
||||
min=2,
|
||||
max=8
|
||||
),
|
||||
io.Combo.Input("mode",
|
||||
options=["average", "weighted", "max", "min"],
|
||||
default="average"
|
||||
)
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output("blended")
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, mode, **kwargs):
|
||||
# Collect all image inputs
|
||||
images = []
|
||||
for key, value in sorted(kwargs.items()):
|
||||
if key.startswith("image"):
|
||||
images.append(value)
|
||||
|
||||
if not images:
|
||||
raise ValueError("No images provided")
|
||||
|
||||
# Stack images
|
||||
stacked = torch.stack(images, dim=0)
|
||||
|
||||
# Blend based on mode
|
||||
if mode == "average":
|
||||
blended = torch.mean(stacked, dim=0)
|
||||
elif mode == "weighted":
|
||||
# Simple linear weighting
|
||||
weights = torch.linspace(1, 0.1, len(images))
|
||||
weights = weights / weights.sum()
|
||||
weights = weights.view(-1, 1, 1, 1, 1)
|
||||
blended = (stacked * weights).sum(dim=0)
|
||||
elif mode == "max":
|
||||
blended = torch.max(stacked, dim=0)[0]
|
||||
elif mode == "min":
|
||||
blended = torch.min(stacked, dim=0)[0]
|
||||
|
||||
return io.NodeOutput(blended, ui=ui.PreviewImage(blended))
|
||||
```
|
||||
|
||||
### Multi-Type Input Example
|
||||
|
||||
```python
|
||||
class UniversalInverterV3(io.ComfyNode):
|
||||
"""Inverts images, masks, or conditioning."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="UniversalInverter_v3",
|
||||
display_name="Universal Inverter",
|
||||
category="utils/invert",
|
||||
inputs=[
|
||||
io.MultiType.Input("input",
|
||||
types=[io.Image, io.Mask, io.Conditioning]
|
||||
),
|
||||
io.Float.Input("strength", default=1.0, min=0.0, max=1.0)
|
||||
],
|
||||
outputs=[
|
||||
io.MultiType.Output("inverted",
|
||||
types=[io.Image, io.Mask, io.Conditioning]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, input, strength):
|
||||
# Detect input type and process accordingly
|
||||
if isinstance(input, torch.Tensor):
|
||||
# Image or Mask
|
||||
if input.dim() == 4: # Image [B,H,W,C]
|
||||
inverted = 1.0 - input
|
||||
inverted = input + (inverted - input) * strength
|
||||
return io.NodeOutput(inverted, ui=ui.PreviewImage(inverted))
|
||||
else: # Mask [H,W] or [B,H,W]
|
||||
inverted = 1.0 - input
|
||||
inverted = input + (inverted - input) * strength
|
||||
return io.NodeOutput(inverted, ui=ui.PreviewMask(inverted))
|
||||
|
||||
elif isinstance(input, list): # Conditioning
|
||||
# Invert conditioning strength
|
||||
inverted = []
|
||||
for cond, data in input:
|
||||
new_data = data.copy()
|
||||
if 'strength' in new_data:
|
||||
new_data['strength'] = 1.0 - new_data['strength']
|
||||
inverted.append((cond, new_data))
|
||||
return io.NodeOutput(inverted)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported input type: {type(input)}")
|
||||
```
|
||||
|
||||
### Custom Type Example
|
||||
|
||||
```python
|
||||
class CustomDataProcessorV3(io.ComfyNode):
|
||||
"""Processes custom data types."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="CustomDataProcessor_v3",
|
||||
display_name="Custom Data Processor",
|
||||
category="utils/custom",
|
||||
inputs=[
|
||||
io.Custom(io_type="MY_CUSTOM_TYPE").Input("custom_data",,
|
||||
tooltip="Custom data type input"
|
||||
),
|
||||
io.Float.Input("scale", default=1.0, min=0.1, max=10.0)
|
||||
],
|
||||
outputs=[
|
||||
io.Custom(io_type="MY_CUSTOM_TYPE").Output("processed_data",
|
||||
tooltip="Processed custom data"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, custom_data, scale):
|
||||
# Process custom data type
|
||||
# Assuming custom_data is a dict with 'value' and 'metadata'
|
||||
processed = {
|
||||
'value': custom_data.get('value', 0) * scale,
|
||||
'metadata': custom_data.get('metadata', {}),
|
||||
'processed': True
|
||||
}
|
||||
|
||||
return io.NodeOutput(processed)
|
||||
```
|
||||
|
||||
## Process Isolation Example
|
||||
|
||||
### Node with Specific Dependencies
|
||||
|
||||
```python
|
||||
# manifest.yaml
|
||||
"""
|
||||
name: scientific_processor
|
||||
version: 1.0.0
|
||||
dependencies:
|
||||
- numpy==1.24.0 # Specific older version needed
|
||||
- scipy==1.10.0
|
||||
- scikit-image==0.20.0
|
||||
isolated: true
|
||||
share_torch: true
|
||||
"""
|
||||
|
||||
# __init__.py
|
||||
from comfy_api.latest import io, io.ComfyNode, io.Schema
|
||||
import numpy as np
|
||||
from skimage import filters
|
||||
|
||||
class ScientificProcessorV3(io.ComfyNode):
|
||||
"""Image processing with scientific libraries."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="ScientificProcessor_v3",
|
||||
display_name="Scientific Processor",
|
||||
category="image/scientific",
|
||||
inputs=[
|
||||
io.Image.Input("image"),
|
||||
io.Combo.Input("filter_type",
|
||||
options=["gaussian", "sobel", "laplacian", "butterworth"],
|
||||
default="gaussian"
|
||||
),
|
||||
io.Float.Input("sigma", default=1.0, min=0.1, max=10.0)
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output("filtered")
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, image, filter_type, sigma):
|
||||
# Convert to numpy
|
||||
img_np = image.cpu().numpy()
|
||||
batch_size = img_np.shape[0]
|
||||
|
||||
results = []
|
||||
for i in range(batch_size):
|
||||
img = img_np[i]
|
||||
|
||||
if filter_type == "gaussian":
|
||||
filtered = filters.gaussian(img, sigma=sigma, channel_axis=-1)
|
||||
elif filter_type == "sobel":
|
||||
gray = np.mean(img, axis=-1)
|
||||
filtered = filters.sobel(gray)
|
||||
filtered = np.stack([filtered]*3, axis=-1)
|
||||
elif filter_type == "laplacian":
|
||||
gray = np.mean(img, axis=-1)
|
||||
filtered = filters.laplace(gray)
|
||||
filtered = np.stack([filtered]*3, axis=-1)
|
||||
elif filter_type == "butterworth":
|
||||
# Frequency domain filtering
|
||||
for c in range(3):
|
||||
channel = img[:,:,c]
|
||||
fft = np.fft.fft2(channel)
|
||||
fft_shift = np.fft.fftshift(fft)
|
||||
# Apply Butterworth filter
|
||||
H = 1 / (1 + (D/sigma)**4) # Simplified
|
||||
filtered_fft = fft_shift * H
|
||||
filtered[:,:,c] = np.real(np.fft.ifft2(np.fft.ifftshift(filtered_fft)))
|
||||
|
||||
results.append(filtered)
|
||||
|
||||
# Convert back to tensor
|
||||
result = torch.from_numpy(np.stack(results)).float()
|
||||
return io.NodeOutput(result, ui=ui.PreviewImage(result))
|
||||
|
||||
# Entry point for pyisolate
|
||||
from pyisolate import ExtensionBase
|
||||
|
||||
class ScientificExtension(ExtensionBase):
|
||||
def on_module_loaded(self, module):
|
||||
self.nodes = {
|
||||
"ScientificProcessor_v3": ScientificProcessorV3
|
||||
}
|
||||
|
||||
def create_extension():
|
||||
return ScientificExtension()
|
||||
```
|
||||
|
||||
## Complete Workflow Example
|
||||
|
||||
```python
|
||||
class TextToImageWorkflowV3(io.ComfyNode):
|
||||
"""Complete text-to-image workflow in one node."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="TextToImageWorkflow_v3",
|
||||
display_name="Text to Image Workflow",
|
||||
category="workflows",
|
||||
description="All-in-one text to image generation",
|
||||
inputs=[
|
||||
io.String.Input("positive_prompt", multiline=True),
|
||||
io.String.Input("negative_prompt", multiline=True, default=""),
|
||||
io.Model.Input("model"),
|
||||
io.Clip.Input("clip"),
|
||||
io.Vae.Input("vae"),
|
||||
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
|
||||
io.Int.Input("steps", default=20, min=1, max=150),
|
||||
io.Float.Input("cfg", default=7.0, min=0.0, max=30.0),
|
||||
io.Combo.Input("sampler_name",
|
||||
options=comfy.samplers.KSampler.SAMPLERS,
|
||||
default="euler"
|
||||
),
|
||||
io.Combo.Input("scheduler",
|
||||
options=comfy.samplers.KSampler.SCHEDULERS,
|
||||
default="normal"
|
||||
),
|
||||
io.Int.Input("width", default=1024, min=64, max=8192, step=8),
|
||||
io.Int.Input("height", default=1024, min=64, max=8192, step=8),
|
||||
io.Int.Input("batch_size", default=1, min=1, max=64)
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output("images", is_output_list=True),
|
||||
io.Latent.Output("latents")
|
||||
],
|
||||
is_output_node=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, positive_prompt, negative_prompt, model, clip, vae,
|
||||
seed, steps, cfg, sampler_name, scheduler,
|
||||
width, height, batch_size):
|
||||
import comfy.samplers
|
||||
|
||||
# Encode prompts
|
||||
positive_cond = clip.encode_from_text(positive_prompt)
|
||||
negative_cond = clip.encode_from_text(negative_prompt)
|
||||
|
||||
# Create empty latent
|
||||
latent = torch.zeros([batch_size, 4, height // 8, width // 8])
|
||||
|
||||
# Set up sampler
|
||||
sampler = comfy.samplers.KSampler(
|
||||
model, steps, cfg, sampler_name, scheduler,
|
||||
positive_cond, negative_cond, latent,
|
||||
denoise=1.0, seed=seed
|
||||
)
|
||||
|
||||
# Sample with progress callback
|
||||
def callback(step, x0, x, total_steps):
|
||||
# Could update progress here
|
||||
pass
|
||||
|
||||
samples = sampler.sample(latent, callback=callback)
|
||||
|
||||
# Decode latents
|
||||
images = vae.decode(samples["samples"])
|
||||
|
||||
return io.NodeOutput(
|
||||
images,
|
||||
samples,
|
||||
ui=ui.PreviewImage(images)
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,530 @@
|
||||
---
|
||||
applyTo: "**/*.py"
|
||||
description: "ComfyUI v3 Migration Guide"
|
||||
---
|
||||
|
||||
# ComfyUI v3 Migration Guide
|
||||
|
||||
This guide helps developers migrate existing v1 nodes to the new v3 schema and take advantage of async execution and process isolation.
|
||||
|
||||
## Quick Start: The Core Changes
|
||||
|
||||
1. **Inherit from `io.ComfyNode`**: Your node class now subclasses `io.ComfyNode`.
|
||||
2. **Use `define_schema`**: All metadata (`INPUT_TYPES`, `CATEGORY`, etc.) moves into a single `@classmethod def define_schema(cls)` that returns an `io.Schema` object.
|
||||
3. **Use `execute`**: The main logic function is now always a `@classmethod def execute(cls, ...)` method.
|
||||
4. **Use Typed I/O**: Inputs and outputs are now strongly-typed objects from the `io` module (e.g., `io.Image.Input(...)`).
|
||||
5. **Return `NodeOutput`**: The `execute` method must return an `io.NodeOutput` instance.
|
||||
6. **Use `NODES_LIST`**: Node registration is done by adding the class to a `NODES_LIST` at the end of the file, replacing `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS`.
|
||||
|
||||
## Step-by-Step Migration
|
||||
|
||||
### Step 1: Class Definition and Schema
|
||||
|
||||
**V1:**
|
||||
```python
|
||||
class Canny:
|
||||
CATEGORY = "image/preprocessors"
|
||||
FUNCTION = "detect_edge"
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"image": ("IMAGE",),
|
||||
"low_threshold": ("FLOAT", {"default": 0.4}),
|
||||
"high_threshold": ("FLOAT", {"default": 0.8}),
|
||||
}}
|
||||
|
||||
def detect_edge(self, image, low_threshold, high_threshold):
|
||||
# ... logic ...
|
||||
return (img_out,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"Canny": Canny}
|
||||
```
|
||||
|
||||
**V3:**
|
||||
```python
|
||||
from comfy_api.latest import io
|
||||
|
||||
class Canny(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="Canny_V3",
|
||||
category="image/preprocessors",
|
||||
inputs=[
|
||||
io.Image.Input("image"),
|
||||
io.Float.Input("low_threshold", default=0.4),
|
||||
io.Float.Input("high_threshold", default=0.8),
|
||||
],
|
||||
outputs=[io.Image.Output()],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, image, low_threshold, high_threshold):
|
||||
# ... logic ...
|
||||
return io.NodeOutput(img_out)
|
||||
|
||||
NODES_LIST = [Canny]
|
||||
```
|
||||
|
||||
### Step 2: Naming and Registration (`node_id`, `display_name`, `NODES_LIST`)
|
||||
|
||||
This is a critical step for ensuring your V3 node coexists with or replaces the V1 version correctly.
|
||||
|
||||
1. **Remove Old Mappings**: Delete the `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` dictionaries.
|
||||
2. **Create `NODES_LIST`**: Create a new list called `NODES_LIST` and add your V3 class to it.
|
||||
3. **Set `node_id`**: The `node_id` in `Schema` **must** be the key from the old `NODE_CLASS_MAPPINGS`.
|
||||
4. **Set `display_name` (Conditionally)**:
|
||||
- Check if a key existed in the old `NODE_DISPLAY_NAME_MAPPINGS`.
|
||||
- **If yes**: Set `display_name` to that value.
|
||||
- **If no**: **Omit** the `display_name` parameter from `Schema` entirely.
|
||||
|
||||
**Example:**
|
||||
|
||||
**V1 Registration:**
|
||||
```python
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"APG": APG,
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"APG": "Adaptive Projected Guidance",
|
||||
}
|
||||
```
|
||||
|
||||
**V3 `define_schema`:**
|
||||
```python
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="APG_V3", # From MAPPINGS key + "_V3"
|
||||
display_name="Adaptive Projected Guidance _V3", # From DISPLAY MAPPINGS value + " _V3"
|
||||
# ... other parameters
|
||||
)
|
||||
|
||||
NODES_LIST = [APG] # ... at end of file
|
||||
```
|
||||
|
||||
### Step 3: Converting I/O
|
||||
|
||||
| V1 Type (`string`) | V3 Class (`io.<Type>`) | Common `Input()` Options (as keyword arguments) |
|
||||
|:-----------------------|:------------------------|:--------------------------------------------------------------------------|
|
||||
| `STRING` | `io.String` | `default`, `multiline`, `dynamic_prompts`, `placeholder` |
|
||||
| `INT` | `io.Int` | `default`, `min`, `max`, `step`, `display_mode`, `control_after_generate` |
|
||||
| `FLOAT` | `io.Float` | `default`, `min`, `max`, `step`, `round`, `display_mode` |
|
||||
| `BOOLEAN` | `io.Boolean` | `default`, `label_on`, `label_off` |
|
||||
| `COMBO` | `io.Combo` | `options`, `default`, `upload`, `image_folder`, `remote` |
|
||||
| (custom) | `io.MultiCombo` | `options`, `default`, `placeholder`, `chip` |
|
||||
| `IMAGE` | `io.Image` | |
|
||||
| `MASK` | `io.Mask` | |
|
||||
| `MESH` | `io.Mesh` | |
|
||||
| `HOOKS` | `io.Hooks` | |
|
||||
| `HOOK_KEYFRAMES` | `io.HookKeyframes` | |
|
||||
| `LATENT` | `io.Latent` | |
|
||||
| `LATENT_OPERATION` | `io.LatentOperation` | |
|
||||
| `LOAD3D_CAMERA` | `io.Load3DCamera` | |
|
||||
| `LOAD_3D` | `io.Load3D` | |
|
||||
| `LOAD_3D_ANIMATION` | `io.Load3DAnimation` | |
|
||||
| `LOSS_MAP` | `io.LossMap` | |
|
||||
| `LORA_MODEL` | `io.LoraModel` | |
|
||||
| `CONDITIONING` | `io.Conditioning` | |
|
||||
| `CLIP` | `io.Clip` | |
|
||||
| `CLIP_VISION_OUTPUT` | `io.ClipVisionOutput` | |
|
||||
| `NOISE` | `io.Noise` | |
|
||||
| `VAE` | `io.Vae` | |
|
||||
| `MODEL` | `io.Model` | |
|
||||
| `CONTROL_NET` | `io.ControlNet` | |
|
||||
| `SAMPLER` | `io.Sampler` | |
|
||||
| `SIGMAS` | `io.Sigmas` | |
|
||||
| `GUIDER` | `io.Guider` | |
|
||||
| `CLIP_VISION` | `io.ClipVision` | |
|
||||
| `UPSCALE_MODEL` | `io.UpscaleModel` | |
|
||||
| `AUDIO` | `io.Audio` | |
|
||||
| `VIDEO` | `io.Video` | |
|
||||
| `VOXEL` | `io.Voxel` | |
|
||||
| `WAN_CAMERA_EMBEDDING` | `io.WanCameraEmbedding` | |
|
||||
| `WEBCAM` | `io.Webcam` | `default`, `socketless` |
|
||||
| `*` | `io.AnyType` | Used for inputs that can accept any type, like the PreviewAny node. |
|
||||
|
||||
#### Advanced Input Types
|
||||
|
||||
**MultiType Input (accepts multiple types):**
|
||||
```python
|
||||
io.MultiType.Input("input", types=[io.Mask, io.Float, io.Int], optional=True)
|
||||
```
|
||||
|
||||
**Combo with Remote Options:**
|
||||
```python
|
||||
io.Combo.Input(
|
||||
"lora_name",
|
||||
options=folder_paths.get_filename_list("loras"),
|
||||
tooltip="The name of the LoRA."
|
||||
)
|
||||
```
|
||||
|
||||
**Optional Parameters:**
|
||||
```python
|
||||
io.Boolean.Input(
|
||||
"case_sensitive",
|
||||
default=True,
|
||||
optional=True, # Makes this input optional
|
||||
tooltip="Whether to use case-sensitive matching"
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
### Step 4: Migrating Logic
|
||||
|
||||
- **Execution Method**: Rename your old `FUNCTION` to `execute` and make it a `@classmethod`.
|
||||
- **Return Value**: Wrap your return tuple in `io.NodeOutput()`. For UI updates, use the `ui` keyword argument: `io.NodeOutput(ui=ui.PreviewImage(image))`.
|
||||
- **State**: Replace `self.variable` with `cls.state.variable`.
|
||||
- **Hidden Inputs**: Replace `prompt` and `unique_id` parameters with `cls.hidden.prompt` and `cls.hidden.unique_id`. Request them in the schema with `hidden=[io.Hidden.prompt, io.Hidden.unique_id]`.
|
||||
- **Optional Methods**: `IS_CHANGED` becomes `fingerprint_inputs`, and `VALIDATE_INPUTS` becomes `validate_inputs`. Both should be `@classmethod`.
|
||||
|
||||
## Common Migration Patterns
|
||||
|
||||
### 1. Hidden Inputs
|
||||
|
||||
**V1:**
|
||||
```python
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
"unique_id": "UNIQUE_ID"
|
||||
}
|
||||
|
||||
def execute(self, ..., prompt=None, unique_id=None):
|
||||
... # Use hidden inputs
|
||||
```
|
||||
|
||||
**V3:**
|
||||
```python
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.unique_id
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def execute(cls, ...):
|
||||
# Access via **cls**
|
||||
prompt = cls.hidden.prompt
|
||||
unique_id = cls.hidden.unique_id
|
||||
```
|
||||
|
||||
### 2. State Management
|
||||
|
||||
**V1:**
|
||||
```python
|
||||
def __init__(self):
|
||||
self.last_seed = None
|
||||
self.cache = {}
|
||||
|
||||
def execute(self, seed, ...):
|
||||
if seed != self.last_seed:
|
||||
self.cache.clear()
|
||||
self.last_seed = seed
|
||||
```
|
||||
|
||||
**V3:**
|
||||
```python
|
||||
@classmethod
|
||||
def execute(cls, seed, ...):
|
||||
if cls.state.last_seed != seed:
|
||||
cls.state.cache = {}
|
||||
cls.state.last_seed = seed
|
||||
```
|
||||
|
||||
### 3. UI Output
|
||||
|
||||
**V1:**
|
||||
```python
|
||||
def execute(self, image):
|
||||
# Save preview manually
|
||||
preview = save_temp_image(image)
|
||||
return {"ui": {"images": preview}, "result": (image,)}
|
||||
```
|
||||
|
||||
**V3:**
|
||||
```python
|
||||
@classmethod
|
||||
def execute(cls, image):
|
||||
return io.NodeOutput(image, ui=ui.PreviewImage(image))
|
||||
```
|
||||
|
||||
### 4. Dynamic Inputs
|
||||
|
||||
**V1:**
|
||||
```python
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
# Complex logic to generate dynamic inputs
|
||||
inputs = {"required": {}}
|
||||
for i in range(get_dynamic_count()):
|
||||
inputs["required"][f"input_{i}"] = ("IMAGE",)
|
||||
return inputs
|
||||
```
|
||||
|
||||
**V3:**
|
||||
```python
|
||||
inputs=[
|
||||
io.AutoGrowDynamic.Input("images",
|
||||
template_input=io.Image.Input("image"),
|
||||
min=1,
|
||||
max=10
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
### 5. Resource Loading
|
||||
|
||||
**V1:**
|
||||
```python
|
||||
def execute(self, model_name):
|
||||
# Direct file loading
|
||||
model_path = folder_paths.get_full_path("checkpoints", model_name)
|
||||
model = comfy.utils.load_torch_file(model_path)
|
||||
```
|
||||
|
||||
**V3:**
|
||||
```python
|
||||
from comfy_api.latest import resources
|
||||
|
||||
@classmethod
|
||||
def execute(cls, model_name):
|
||||
# Cached resource loading
|
||||
model = cls.resources.get(
|
||||
resources.TorchDictFolderFilename("checkpoints", model_name)
|
||||
)
|
||||
```
|
||||
|
||||
## Making Nodes Async
|
||||
|
||||
### Basic Async Node
|
||||
|
||||
```python
|
||||
class AsyncNodeV3(io.ComfyNode):
|
||||
@classmethod
|
||||
async def execute(cls, image, url):
|
||||
# Network request without blocking
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
data = await response.json()
|
||||
|
||||
# Process with the data
|
||||
result = process_image_with_data(image, data)
|
||||
return io.NodeOutput(result)
|
||||
```
|
||||
|
||||
### Progress Tracking
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
async def execute(cls, images, unique_id):
|
||||
from comfy.utils import ProgressBar
|
||||
|
||||
batch_size = images.shape[0]
|
||||
pbar = ProgressBar(batch_size, node_id=unique_id)
|
||||
|
||||
results = []
|
||||
for i in range(batch_size):
|
||||
# Async processing
|
||||
result = await process_single(images[i])
|
||||
results.append(result)
|
||||
pbar.update(1)
|
||||
|
||||
return io.NodeOutput(torch.cat(results))
|
||||
```
|
||||
|
||||
## Enabling Process Isolation
|
||||
|
||||
### 1. Create manifest.yaml
|
||||
|
||||
```yaml
|
||||
name: my_custom_nodes
|
||||
version: 1.0.0
|
||||
description: My custom node collection
|
||||
author: Your Name
|
||||
dependencies:
|
||||
- numpy==1.26.4
|
||||
- scikit-image>=0.22.0
|
||||
- opencv-python
|
||||
isolated: true
|
||||
share_torch: true
|
||||
```
|
||||
|
||||
### 2. Update __init__.py
|
||||
|
||||
```python
|
||||
from pyisolate import ExtensionBase
|
||||
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
class MyNodesExtension(ExtensionBase):
|
||||
def on_module_loaded(self, module):
|
||||
# Nodes are automatically registered
|
||||
pass
|
||||
|
||||
async def get_node_mappings(self):
|
||||
return NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
# Extension entry point
|
||||
def create_extension():
|
||||
return MyNodesExtension()
|
||||
```
|
||||
|
||||
## Practical Migration Examples
|
||||
|
||||
### Complete String Node Conversion
|
||||
|
||||
This example shows a full conversion of the StringConcatenate node from v1 to v3:
|
||||
|
||||
**V1 Implementation:**
|
||||
```python
|
||||
class StringConcatenate():
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"string_a": (IO.STRING, {"multiline": True}),
|
||||
"string_b": (IO.STRING, {"multiline": True}),
|
||||
"delimiter": (IO.STRING, {"multiline": False, "default": ""})
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (IO.STRING,)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "utils/string"
|
||||
|
||||
def execute(self, string_a, string_b, delimiter, **kwargs):
|
||||
return delimiter.join((string_a, string_b)),
|
||||
```
|
||||
|
||||
**V3 Implementation:**
|
||||
```python
|
||||
from comfy_api.latest import io, ui
|
||||
|
||||
class StringConcatenate(io.ComfyNode):
|
||||
"""Concatenates two strings with an optional delimiter between them."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="StringConcatenate",
|
||||
display_name="String Concatenate",
|
||||
category="utils/string",
|
||||
description="Concatenates two strings together with an optional delimiter between them.",
|
||||
inputs=[
|
||||
io.String.Input(
|
||||
"string_a",
|
||||
display_name="String A",
|
||||
multiline=True,
|
||||
tooltip="The first string to concatenate"
|
||||
),
|
||||
io.String.Input(
|
||||
"string_b",
|
||||
display_name="String B",
|
||||
multiline=True,
|
||||
tooltip="The second string to concatenate"
|
||||
),
|
||||
io.String.Input(
|
||||
"delimiter",
|
||||
display_name="Delimiter",
|
||||
default="",
|
||||
multiline=False,
|
||||
tooltip="The delimiter to insert between the two strings (empty by default)"
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(
|
||||
"concatenated",
|
||||
display_name="Concatenated String",
|
||||
tooltip="The result of concatenating string_a and string_b with the delimiter"
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, string_a: str, string_b: str, delimiter: str) -> io.NodeOutput:
|
||||
"""Concatenates two strings with an optional delimiter."""
|
||||
result = delimiter.join((string_a, string_b))
|
||||
return io.NodeOutput(result)
|
||||
```
|
||||
|
||||
### Replacing V1 Nodes Strategy
|
||||
|
||||
When replacing v1 nodes with v3 implementations:
|
||||
|
||||
1. **Keep Original Node Names**: Don't add "V3" suffix to maintain compatibility
|
||||
2. **Preserve All Parameters**: Keep same parameter names and defaults
|
||||
3. **Maintain Return Structure**: v3 automatically generates v1-compatible returns
|
||||
4. **Test Workflow Compatibility**: Ensure existing workflows continue to work
|
||||
|
||||
Example migration workflow:
|
||||
```bash
|
||||
# 1. Create new branch
|
||||
git checkout -b v3-node-migration
|
||||
|
||||
# 2. Backup original
|
||||
cp nodes_original.py nodes_original.py.bak
|
||||
|
||||
# 3. Replace with v3 version
|
||||
cp nodes_v3.py nodes_original.py
|
||||
|
||||
# 4. Test with existing workflows
|
||||
comfy-cli test-workflows ./test-workflows/
|
||||
```
|
||||
|
||||
## Testing Your Migration
|
||||
|
||||
### 1. Backward Compatibility Test
|
||||
|
||||
```python
|
||||
# Your v3 node should work with v1 calls
|
||||
def test_v1_compatibility():
|
||||
node = MyNodeV3()
|
||||
inputs = node.INPUT_TYPES()
|
||||
assert "required" in inputs
|
||||
assert hasattr(node, "FUNCTION")
|
||||
assert hasattr(node, "RETURN_TYPES")
|
||||
```
|
||||
|
||||
### 2. Async Execution Test
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def test_async_execution():
|
||||
result = await MyAsyncNode.execute(image=test_image)
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
### 3. Isolation Test
|
||||
|
||||
```bash
|
||||
# Test with conflicting dependencies
|
||||
comfy-cli test-node --isolated my_custom_nodes
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep nodes stateless** - Use `cls.state` for any mutable data
|
||||
2. **Make I/O operations async** - Network, disk, database operations
|
||||
3. **Use resource caching** - Via `cls.resources.get()`
|
||||
4. **Declare all dependencies** - In manifest.yaml
|
||||
5. **Test both sync and async** - Ensure compatibility
|
||||
6. **Document type changes** - Help users update workflows
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue: State not persisting
|
||||
**Solution:** Use `cls.state` instead of instance variables
|
||||
|
||||
### Issue: Hidden inputs not working
|
||||
**Solution:** Access via `cls.hidden.unique_id` not function parameters
|
||||
|
||||
### Issue: Async not executing
|
||||
**Solution:** Ensure method is `async def` and use `await` for async calls
|
||||
|
||||
### Issue: Import errors in isolation
|
||||
**Solution:** Add all dependencies to manifest.yaml
|
||||
|
||||
### Issue: Tensors not sharing
|
||||
**Solution:** Enable `share_torch: true` in manifest.yaml
|
||||
@@ -0,0 +1,635 @@
|
||||
---
|
||||
applyTo: "**/*.py"
|
||||
description: "ComfyUI v3 API Reference"
|
||||
---
|
||||
|
||||
# ComfyUI v3 API Reference
|
||||
|
||||
Complete reference for the ComfyUI v3 node API, including all types, methods, and decorators.
|
||||
|
||||
## Core Classes
|
||||
|
||||
### ComfyNodeV3
|
||||
|
||||
Base class for all v3 nodes.
|
||||
|
||||
```python
|
||||
from comfy_api.latest import io
|
||||
|
||||
class CustomNode(io.ComfyNode):
|
||||
# Class properties set during execution
|
||||
state: NodeState # Persistent state storage
|
||||
resources: Resources # Resource loader with caching
|
||||
hidden: HiddenHolder # Access to hidden inputs
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def define_schema(cls) -> io.ComfyNode:
|
||||
"""Define node schema. Must be overridden."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def execute(cls, **kwargs) -> io.NodeOutput:
|
||||
"""Execute node logic. Can be async."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def validate_inputs(cls, **kwargs) -> bool:
|
||||
"""Optional: Validate inputs before execution."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def fingerprint_inputs(cls, **kwargs) -> Any:
|
||||
"""Optional: Generate a fingerprint for caching."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def GET_SERIALIZERS(cls) -> list[Serializer]:
|
||||
"""Optional: Define custom serializers."""
|
||||
return []
|
||||
```
|
||||
|
||||
### io.ComfyNode
|
||||
|
||||
Node definition schema.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class io.ComfyNode:
|
||||
node_id: str # Globally unique ID
|
||||
display_name: str = None # UI display name
|
||||
category: str = "sd" # Node category
|
||||
inputs: list[InputV3] = None # Input definitions
|
||||
outputs: list[OutputV3] = None # Output definitions
|
||||
hidden: list[Hidden] = None # Hidden inputs
|
||||
description: str = "" # Tooltip description
|
||||
is_input_list: bool = False # Handle list inputs
|
||||
is_output_node: bool = False # Force execution
|
||||
is_deprecated: bool = False # Mark as deprecated
|
||||
is_experimental: bool = False # Mark as experimental
|
||||
is_api_node: bool = False # API node flag
|
||||
not_idempotent: bool = False # Disable caching
|
||||
```
|
||||
|
||||
### NodeOutput
|
||||
|
||||
Structured return value from `execute`.
|
||||
|
||||
```python
|
||||
class NodeOutput:
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any, # Output values
|
||||
ui: UIOutput | dict = None, # UI elements
|
||||
expand: dict = None, # Subgraph expansion
|
||||
block_execution: str = None # Execution blocker
|
||||
):
|
||||
pass
|
||||
```
|
||||
|
||||
## Input Types
|
||||
|
||||
### Basic Inputs
|
||||
|
||||
```python
|
||||
# Integer input
|
||||
io.Int.Input(
|
||||
id: str,
|
||||
display_name: str = None,
|
||||
optional: bool = False,
|
||||
tooltip: str = None,
|
||||
lazy: bool = None,
|
||||
default: int = None,
|
||||
min: int = None,
|
||||
max: int = None,
|
||||
step: int = None,
|
||||
control_after_generate: bool = None,
|
||||
display_mode: NumberDisplay = None,
|
||||
socketless: bool = None,
|
||||
force_input: bool = None
|
||||
)
|
||||
|
||||
# Float input
|
||||
io.Float.Input(
|
||||
id: str,
|
||||
display_name: str = None,
|
||||
optional: bool = False,
|
||||
tooltip: str = None,
|
||||
lazy: bool = None,
|
||||
default: float = None,
|
||||
min: float = None,
|
||||
max: float = None,
|
||||
step: float = None,
|
||||
round: float = None,
|
||||
display_mode: NumberDisplay = None,
|
||||
socketless: bool = None,
|
||||
force_input: bool = None
|
||||
)
|
||||
|
||||
# String input
|
||||
io.String.Input(
|
||||
id: str,
|
||||
display_name: str = None,
|
||||
optional: bool = False,
|
||||
tooltip: str = None,
|
||||
lazy: bool = None,
|
||||
multiline: bool = False,
|
||||
placeholder: str = None,
|
||||
default: str = None,
|
||||
dynamic_prompts: bool = None,
|
||||
socketless: bool = None,
|
||||
force_input: bool = None
|
||||
)
|
||||
|
||||
# Boolean input
|
||||
io.Boolean.Input(
|
||||
id: str,
|
||||
display_name: str = None,
|
||||
optional: bool = False,
|
||||
tooltip: str = None,
|
||||
lazy: bool = None,
|
||||
default: bool = None,
|
||||
label_on: str = None,
|
||||
label_off: str = None,
|
||||
socketless: bool = None,
|
||||
force_input: bool = None
|
||||
)
|
||||
|
||||
# Combo (dropdown) input
|
||||
io.Combo.Input(
|
||||
id: str,
|
||||
options: list[str] = None,
|
||||
display_name: str = None,
|
||||
optional: bool = False,
|
||||
tooltip: str = None,
|
||||
lazy: bool = None,
|
||||
default: str = None,
|
||||
control_after_generate: bool = None,
|
||||
upload: UploadType = None,
|
||||
image_folder: FolderType = None,
|
||||
remote: RemoteOptions = None,
|
||||
socketless: bool = None
|
||||
)
|
||||
|
||||
# Multi-select combo
|
||||
io.MultiCombo.Input(
|
||||
id: str,
|
||||
options: list[str],
|
||||
display_name: str = None,
|
||||
optional: bool = False,
|
||||
tooltip: str = None,
|
||||
lazy: bool = None,
|
||||
default: list[str] = None,
|
||||
placeholder: str = None,
|
||||
chip: bool = None,
|
||||
control_after_generate: bool = None,
|
||||
socketless: bool = None
|
||||
)
|
||||
# cusotm type
|
||||
io.Custom(io_type="MY_TYPE").Input(
|
||||
id: str,
|
||||
display_name: str = None,
|
||||
optional: bool = False,
|
||||
tooltip: str = None,
|
||||
lazy: bool = None,
|
||||
placeholder: str = None,
|
||||
)
|
||||
```
|
||||
|
||||
### ComfyUI Types
|
||||
|
||||
```python
|
||||
# Core types
|
||||
io.Image.Input(id, ...) # Type: torch.Tensor [B,H,W,C]
|
||||
io.Mask.Input(id, ...) # Type: torch.Tensor [H,W] or [B,H,W]
|
||||
io.Latent.Input(id, ...) # Type: dict with 'samples' tensor
|
||||
io.Conditioning.Input(id, ...) # Type: list[tuple[tensor, dict]]
|
||||
io.Model.Input(id, ...) # Type: ModelPatcher
|
||||
io.Clip.Input(id, ...) # Type: CLIP
|
||||
io.Vae.Input(id, ...) # Type: VAE
|
||||
io.ControlNet.Input(id, ...) # Type: ControlNet
|
||||
|
||||
# Sampling types
|
||||
io.Sampler.Input(id, ...) # Type: Sampler
|
||||
io.Sigmas.Input(id, ...) # Type: torch.Tensor
|
||||
io.Noise.Input(id, ...) # Type: torch.Tensor
|
||||
io.Guider.Input(id, ...) # Type: CFGGuider
|
||||
|
||||
# Additional types
|
||||
io.ClipVision.Input(id, ...) # Type: ClipVisionModel
|
||||
io.ClipVisionOutput.Input(id, ...) # Type: ClipVisionOutput
|
||||
io.StyleModel.Input(id, ...) # Type: StyleModel
|
||||
io.Gligen.Input(id, ...) # Type: ModelPatcher
|
||||
io.UpscaleModel.Input(id, ...) # Type: ImageModelDescriptor
|
||||
io.Audio.Input(id, ...) # Type: dict with 'waveform' and 'sample_rate'
|
||||
io.Video.Input(id, ...) # Type: VideoInput
|
||||
io.Webcam.Input(id, ...) # Type: str (filepath)
|
||||
io.WanCameraEmbedding.Input(id, ...) # Type: torch.Tensor
|
||||
io.LoraModel.Input(id, ...) # Type: dict[str, Tensor]
|
||||
io.Hooks.Input(id, ...) # Type: HookGroup
|
||||
io.HookKeyframes.Input(id, ...) # Type: HookKeyframeGroup
|
||||
io.SVG.Input(id, ...) # Type: SVG (custom class)
|
||||
io.Voxel.Input(id, ...) # Type: Voxel data (custom class)
|
||||
io.Mesh.Input(id, ...) # Type: Mesh data (custom class)
|
||||
```
|
||||
|
||||
### Advanced Inputs
|
||||
|
||||
```python
|
||||
# Multi-type input (accepts multiple types)
|
||||
io.MultiType.Input(
|
||||
id: str | InputV3, # Can override from existing input
|
||||
types: list[type[ComfyType]],
|
||||
display_name: str = None,
|
||||
optional: bool = False,
|
||||
tooltip: str = None,
|
||||
lazy: bool = None
|
||||
)
|
||||
|
||||
# Dynamic growing input
|
||||
io.AutogrowDynamic.Input(
|
||||
id: str,
|
||||
template_input: InputV3, # Template for each new input
|
||||
min: int = 1, # Minimum inputs
|
||||
max: int = None # Maximum inputs
|
||||
)
|
||||
|
||||
# Custom type
|
||||
@io.comfytype(io_type="MY_CUSTOM")
|
||||
class MyCustom:
|
||||
Type = MyDataClass
|
||||
class Input(io.InputV3):
|
||||
...
|
||||
class Output(io.OutputV3):
|
||||
...
|
||||
```
|
||||
|
||||
## Output Types
|
||||
|
||||
```python
|
||||
# Basic output
|
||||
io.Image.Output(
|
||||
id: str = None,
|
||||
display_name: str = None,
|
||||
tooltip: str = None,
|
||||
is_output_list: bool = False # Output is list
|
||||
)
|
||||
|
||||
# All ComfyUI types have corresponding outputs
|
||||
io.Mask.Output(id, ...)
|
||||
io.Latent.Output(id, ...)
|
||||
io.Model.Output(id, ...)
|
||||
io.Clip.Output(id, ...)
|
||||
io.Vae.Output(id, ...)
|
||||
io.Conditioning.Output(id, ...)
|
||||
io.String.Output(id, ...)
|
||||
io.Int.Output(id, ...)
|
||||
io.Float.Output(id, ...)
|
||||
io.Boolean.Output(id, ...)
|
||||
# ... etc
|
||||
```
|
||||
|
||||
## Hidden Inputs
|
||||
|
||||
```python
|
||||
from comfy_api.latest import Hidden
|
||||
|
||||
# Available hidden inputs
|
||||
Hidden.unique_id # Node's unique ID
|
||||
Hidden.prompt # Complete prompt
|
||||
Hidden.extra_pnginfo # PNG metadata dict
|
||||
Hidden.dynprompt # Dynamic prompt object
|
||||
Hidden.auth_token_comfy_org # ComfyOrg auth token
|
||||
Hidden.api_key_comfy_org # ComfyOrg API key
|
||||
|
||||
# Usage in schema
|
||||
hidden=[
|
||||
Hidden.unique_id,
|
||||
Hidden.prompt
|
||||
]
|
||||
|
||||
# Access in execute
|
||||
unique_id = cls.hidden.unique_id
|
||||
prompt = cls.hidden.prompt
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
```python
|
||||
# NodeState interface
|
||||
class NodeState:
|
||||
def get_value(self, key: str) -> Any
|
||||
def set_value(self, key: str, value: Any)
|
||||
def pop(self, key: str) -> Any
|
||||
def __contains__(self, key: str) -> bool
|
||||
|
||||
# Attribute access
|
||||
cls.state.my_value = 42
|
||||
value = cls.state.my_value
|
||||
|
||||
# Dictionary access
|
||||
cls.state["key"] = "value"
|
||||
value = cls.state["key"]
|
||||
```
|
||||
|
||||
## Practical Input/Output Examples
|
||||
|
||||
### Enhanced Documentation with Tooltips and Display Names
|
||||
|
||||
```python
|
||||
# String input with full documentation
|
||||
io.String.Input(
|
||||
"prompt",
|
||||
display_name="Text Prompt",
|
||||
multiline=True,
|
||||
default="A beautiful landscape",
|
||||
tooltip="Enter the text description for image generation",
|
||||
placeholder="Type your prompt here..."
|
||||
)
|
||||
|
||||
# Integer with constraints and UI hints
|
||||
io.Int.Input(
|
||||
"steps",
|
||||
display_name="Sampling Steps",
|
||||
default=20,
|
||||
min=1,
|
||||
max=150,
|
||||
tooltip="Number of denoising steps. Higher values take longer but may produce better results",
|
||||
display_mode=io.NumberDisplay.slider
|
||||
)
|
||||
|
||||
# Combo with dynamic options
|
||||
io.Combo.Input(
|
||||
"checkpoint",
|
||||
options=folder_paths.get_filename_list("checkpoints"),
|
||||
display_name="Model Checkpoint",
|
||||
tooltip="Select the AI model to use for generation"
|
||||
)
|
||||
|
||||
# Output with documentation
|
||||
io.Image.Output(
|
||||
"generated_image",
|
||||
display_name="Generated Image",
|
||||
tooltip="The final generated image based on your prompt"
|
||||
)
|
||||
|
||||
# Combo with dynamic options and file upload
|
||||
io.Combo.Input(
|
||||
"audio_file",
|
||||
options=sorted(folder_paths.filter_files_content_types(os.listdir(folder_paths.get_input_directory()), ["audio", "video"])),
|
||||
display_name="Audio File",
|
||||
tooltip="Select an audio file or upload a new one",
|
||||
upload=io.UploadType.audio
|
||||
)
|
||||
```
|
||||
|
||||
### Return Pattern with NodeOutput
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def execute(cls, text: str, count: int) -> io.NodeOutput:
|
||||
# Single output
|
||||
result = process_text(text, count)
|
||||
return io.NodeOutput(result)
|
||||
|
||||
# Multiple outputs
|
||||
image, mask = generate_image_and_mask(text)
|
||||
return io.NodeOutput(image, mask)
|
||||
|
||||
# With UI preview
|
||||
image = generate_image(text)
|
||||
return io.NodeOutput(image, ui=ui.PreviewImage(image))
|
||||
|
||||
# With multiple UI elements
|
||||
images = batch_generate(text, count)
|
||||
previews = [ui.PreviewImage(img) for img in images]
|
||||
return io.NodeOutput(images, ui={"images": previews})
|
||||
```
|
||||
|
||||
## Resource Management
|
||||
|
||||
```python
|
||||
# Load cached resources
|
||||
from comfy_api.latest import resources
|
||||
|
||||
# Load torch file
|
||||
model = cls.resources.get(
|
||||
resources.TorchDictFolderFilename(
|
||||
folder_name="checkpoints", # Folder category
|
||||
file_name="model.safetensors"
|
||||
)
|
||||
)
|
||||
|
||||
# With default value
|
||||
model = cls.resources.get(key, default=None)
|
||||
|
||||
# Custom resource types (future)
|
||||
class MyResourceKey(ResourceKey):
|
||||
Type = MyResourceType
|
||||
def __init__(self, ...):
|
||||
pass
|
||||
```
|
||||
|
||||
## UI Output Classes
|
||||
|
||||
```python
|
||||
from comfy_api.latest import ui
|
||||
|
||||
# Image preview
|
||||
ui.PreviewImage(
|
||||
image: torch.Tensor,
|
||||
animated: bool = False
|
||||
)
|
||||
|
||||
# Mask preview
|
||||
ui.PreviewMask(
|
||||
mask: torch.Tensor,
|
||||
animated: bool = False
|
||||
)
|
||||
|
||||
# Audio preview
|
||||
ui.PreviewAudio(
|
||||
values: list[SavedResult | dict]
|
||||
)
|
||||
|
||||
# Text output
|
||||
ui.PreviewText(
|
||||
value: str
|
||||
)
|
||||
|
||||
# 3D preview
|
||||
ui.PreviewUI3D(
|
||||
values: list[SavedResult | dict]
|
||||
)
|
||||
```
|
||||
|
||||
## Decorators and Helpers
|
||||
|
||||
```python
|
||||
# Create custom ComfyType
|
||||
@io.comfytype(io_type="CUSTOM_TYPE")
|
||||
class CustomType:
|
||||
Type = CustomClass
|
||||
class Input(io.InputV3):
|
||||
...
|
||||
class Output(io.OutputV3):
|
||||
...
|
||||
|
||||
# Custom serializer
|
||||
class MySerializer(Serializer, io_type="MY_TYPE"):
|
||||
@classmethod
|
||||
def serialize(cls, obj: Any) -> str:
|
||||
return json.dumps(obj)
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, s: str) -> Any:
|
||||
return json.loads(s)
|
||||
```
|
||||
|
||||
## Async Support
|
||||
|
||||
```python
|
||||
# Async execute
|
||||
class AsyncNode(io.ComfyNode):
|
||||
@classmethod
|
||||
async def execute(cls, **kwargs):
|
||||
result = await async_operation()
|
||||
return io.NodeOutput(result)
|
||||
|
||||
# Async validation
|
||||
@classmethod
|
||||
async def VALIDATE_INPUTS(cls, **kwargs):
|
||||
is_valid = await check_validity()
|
||||
return True if is_valid else "Error message"
|
||||
|
||||
# Async lazy check
|
||||
async def check_lazy_status(cls, **kwargs):
|
||||
needed = await determine_needed_inputs()
|
||||
return needed # List of input names
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```python
|
||||
from comfy_api.latest import io, ui, resources, io.ComfyNode, io.ComfyNode
|
||||
import torch
|
||||
|
||||
class AdvancedNodeV3(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.ComfyNode(
|
||||
node_id="AdvancedNode",
|
||||
display_name="Advanced Node",
|
||||
category="examples/advanced",
|
||||
description="Demonstrates v3 features",
|
||||
inputs=[
|
||||
# Basic inputs
|
||||
io.Image.Input("image", tooltip="Input image"),
|
||||
io.Model.Input("model", tooltip="Model to use"),
|
||||
|
||||
# Configured inputs
|
||||
io.Float.Input("strength",
|
||||
default=0.75,
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
step=0.05,
|
||||
display_mode=io.NumberDisplay.slider
|
||||
),
|
||||
|
||||
# Multi-type
|
||||
io.MultiType.Input("flexible",
|
||||
types=[io.Image, io.Mask, io.Latent],
|
||||
optional=True
|
||||
),
|
||||
|
||||
# Dynamic
|
||||
io.AutoGrowDynamic.Input("extra_images",
|
||||
template_input=io.Image.Input("img"),
|
||||
min=0,
|
||||
max=5
|
||||
)
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output("result", tooltip="Processed image"),
|
||||
io.Latent.Output("latent", is_output_list=True)
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.unique_id,
|
||||
io.Hidden.prompt
|
||||
],
|
||||
is_output_node=True,
|
||||
is_experimental=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, image, model, strength, flexible=None, **kwargs):
|
||||
# Access state
|
||||
if cls.state.last_model != model:
|
||||
cls.state.cache = {}
|
||||
cls.state.last_model = model
|
||||
|
||||
# Load resources
|
||||
weights = cls.resources.get(
|
||||
resources.TorchDictFolderFilename("loras", "style.safetensors"),
|
||||
default=None
|
||||
)
|
||||
|
||||
# Access hidden
|
||||
node_id = cls.hidden.unique_id
|
||||
|
||||
# Process async
|
||||
result = await process_with_model(image, model, strength)
|
||||
|
||||
# Handle dynamic inputs
|
||||
extra_images = [v for k, v in kwargs.items() if k.startswith("extra_")]
|
||||
|
||||
# Return with UI
|
||||
return io.NodeOutput(
|
||||
result,
|
||||
[latent],
|
||||
ui=ui.PreviewImage(result)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def fingerprint_inputs(cls, strength, **kwargs):
|
||||
if strength < 0.1:
|
||||
return "Strength too low for good results"
|
||||
return True
|
||||
```
|
||||
|
||||
## Type Reference
|
||||
|
||||
### Type Mappings
|
||||
|
||||
| v3 Type | Python Type | Shape/Format |
|
||||
|---------|------------|--------------|
|
||||
| `io.Image.Type` | `torch.Tensor` | `[B,H,W,C]` float32 0-1 |
|
||||
| `io.Mask.Type` | `torch.Tensor` | `[H,W]` or `[B,H,W]` float32 |
|
||||
| `io.Latent.Type` | `dict` | `{"samples": tensor, ...}` |
|
||||
| `io.Conditioning.Type` | `list` | `[(tensor, dict), ...]` |
|
||||
| `io.Audio.Type` | `dict` | `{"waveform": tensor, "sample_rate": int}` |
|
||||
| `io.Int.Type` | `int` | Python integer |
|
||||
| `io.Float.Type` | `float` | Python float |
|
||||
| `io.String.Type` | `str` | Python string |
|
||||
| `io.Boolean.Type` | `bool` | Python boolean |
|
||||
|
||||
### Enum Types
|
||||
|
||||
```python
|
||||
# Number display modes
|
||||
io.NumberDisplay.number # Standard input
|
||||
io.NumberDisplay.slider # Slider widget
|
||||
io.NumberDisplay.color # Color picker widget
|
||||
|
||||
# Folder types
|
||||
io.FolderType.input # Input folder
|
||||
io.FolderType.output # Output folder
|
||||
io.FolderType.temp # Temp folder
|
||||
|
||||
# Upload types
|
||||
io.UploadType.image
|
||||
io.UploadType.audio
|
||||
io.UploadType.video
|
||||
io.UploadType.model
|
||||
```
|
||||
Submodule ComfyUI-Easy-Use-Frontend updated: dd43143c91...d0b0a207dd
+11
-1
@@ -19,7 +19,7 @@
|
||||
- 增加了预采样参数配置的节点,可与采样节点分离,更方便预览。
|
||||
- 支持通配符与Lora的提示词节点,如需使用Lora Block Weight用法,需先保证自定义节点包中安装了 [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack)
|
||||
- 可多选的风格化提示词选择器,默认是Fooocus的样式json,可自定义json放在styles底下,samples文件夹里可放预览图(名称和name一致,图片文件名如有空格需转为下划线'_')
|
||||
- 加载器可开启A1111提示词风格模式,可重现与webui生成近乎相同的图像,需先安装 [ComfyUI_smZNodes](https://github.com/shiimizu/ComfyUI_smZNodes)
|
||||
- 加载器可开启A1111提示词风格模式,可重现与webui生成近乎相同的图像
|
||||
- 可使用`easy latentNoisy`或`easy preSamplingNoiseIn`节点实现对潜空间的噪声注入
|
||||
- 简化 SD1.x、SD2.x、SDXL、SVD、Zero123等流程
|
||||
- 简化 Stable Cascade [示例参考](https://github.com/yolain/ComfyUI-Yolain-Workflows?tab=readme-ov-file#1-13-stable-cascade)
|
||||
@@ -52,6 +52,16 @@ git clone https://github.com/yolain/ComfyUI-Easy-Use
|
||||
|
||||
## 📜 更新日志
|
||||
|
||||
**v1.3.6**
|
||||
|
||||
- 恢复 `easy showAnything` 对于列表类型的支持(但一些情况下展示庞大数据时仍会导致ComfyUI崩溃)
|
||||
- 修复自定义小部件以支持子图和 Nodes 2.0 #942
|
||||
- 添加 `easy multiAngle` 节点
|
||||
- 将 `prompt.py` 转换为 V3 Schema
|
||||
- 修复 `easy humanSegmentation` 错误
|
||||
- 添加 `easy stringJoinLines`、`easy stringToIntList`、`easy simpleMath`
|
||||
- 修复 `easy ifElse` 和 `easy anythingIndexSwitch` 在某些环境下失败的问题
|
||||
|
||||
**v1.3.5**
|
||||
|
||||
- 修复`isNone`
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
- Added a node for pre-sampling parameter configuration, which can be separated from the sampling node for easier previewing
|
||||
- Wildcards and lora's are supported, for Lora Block Weight usage, ensure that the custom node package has the [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack)
|
||||
- Multi-selectable styled cue word selector, default is Fooocus style json, custom json can be placed under styles, samples folder can be placed in the preview image (name and name consistent, image file name such as spaces need to be converted to underscores '_')
|
||||
- The loader enables the A1111 prompt mode, which reproduces nearly identical images to those generated by webui, and needs to be installed [ComfyUI_smZNodes](https://github.com/shiimizu/ComfyUI_smZNodes) first.
|
||||
- The loader enables the A1111 prompt mode, which reproduces nearly identical images to those generated by webui.
|
||||
- Noise injection into the latent space can be achieved using the `easy latentNoisy` or `easy preSamplingNoiseIn` node
|
||||
- Simplified processes for SD1.x, SD2.x, SDXL, SVD, Zero123, etc. [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableDiffusion)
|
||||
- Simplified Stable Cascade [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableCascade)
|
||||
@@ -47,6 +47,16 @@ Double-click install.bat to install the required dependencies
|
||||
|
||||
## 📜 Changelog
|
||||
|
||||
**v1.3.6**
|
||||
|
||||
- Restored `easy showAnything` support for list types (but displaying large data in some cases may still cause ComfyUI to crash)
|
||||
- Fix custom widgets to support subgraph and Nodes 2.0 #942
|
||||
- Add `easy multiAngle` node
|
||||
- Convert `prompt.py` to V3 Schema
|
||||
- Fix `easy humanSegmentation` error
|
||||
- Add `easy stringJoinLines`,`easy stringToIntList`, `easy simpleMath`
|
||||
- Fix `easy ifElse` and `easy anythingIndexSwitch` fails in certain environments
|
||||
|
||||
**v1.3.5**
|
||||
|
||||
- Fix `isNone`
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
__version__ = "1.3.4"
|
||||
__version__ = "1.3.6"
|
||||
|
||||
import yaml
|
||||
import json
|
||||
|
||||
@@ -469,6 +469,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy multiAngle":{
|
||||
"display_name": "Multi Angle Prompt",
|
||||
"inputs": {
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"1":{
|
||||
"name": "params"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy fullLoader": {
|
||||
"display_name": "EasyLoader (Full)",
|
||||
"inputs": {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"Hotkeys": "快捷键",
|
||||
"Nodes": "节点相关",
|
||||
"NodesMap": "管理节点组",
|
||||
"StylesSelector": "样式选择器"
|
||||
"StylesSelector": "样式选择器",
|
||||
"MultiAngle": "摄影机多角度提示词"
|
||||
},
|
||||
"nodeCategories": {
|
||||
"Util": "工具",
|
||||
|
||||
@@ -393,6 +393,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy multiAngle":{
|
||||
"display_name": "多视角提示词",
|
||||
"inputs": {
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "提示词"
|
||||
},
|
||||
"1":{
|
||||
"name": "参数"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy fullLoader": {
|
||||
"display_name": "简易加载器 (完整版)",
|
||||
"inputs": {
|
||||
|
||||
@@ -71,5 +71,16 @@
|
||||
"Grid": "网格",
|
||||
"List": "列表"
|
||||
}
|
||||
},
|
||||
"EasyUse_MultiAngle_InvertRotate": {
|
||||
"name": "启用反转旋转模式",
|
||||
"tooltip": "在多角度节点中启用反转旋转模式,使旋转方向与大多数3D软件一致"
|
||||
},
|
||||
"EasyUse_MultiAngle_HollowMode": {
|
||||
"name": "启用多角度镂空展示模式",
|
||||
"tooltip": "在多角度节点中启用镂空展示模式,可以更直观地查看相机角度"
|
||||
},
|
||||
"EasyUse_MultiAngle_AddAnglePrompt": {
|
||||
"name": "启用添加多角度提示词"
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Math utility functions for formula evaluation
|
||||
"""
|
||||
import math
|
||||
import re
|
||||
|
||||
def evaluate_formula(formula: str, a=0, b=0, c=0, d=0) -> float:
|
||||
"""
|
||||
计算字符串数学公式
|
||||
|
||||
支持的运算符和函数:
|
||||
- 基本运算:+, -, *, /, //, %, **
|
||||
- 比较运算:>, <, >=, <=, ==, !=
|
||||
- 数学函数:abs, pow, round, ceil, floor, sqrt, exp, log, log10
|
||||
- 三角函数:sin, cos, tan, asin, acos, atan
|
||||
- 常量:pi, e
|
||||
|
||||
Args:
|
||||
formula: 数学公式字符串,可以使用变量a、b、c、d
|
||||
a: 变量a的值
|
||||
b: 变量b的值
|
||||
c: 变量c的值
|
||||
d: 变量d的值
|
||||
|
||||
Returns:
|
||||
计算结果
|
||||
|
||||
Examples:
|
||||
>>> evaluate_formula("a + b", 1, 2)
|
||||
3.0
|
||||
>>> evaluate_formula("pow(a, 2)", 5)
|
||||
25.0
|
||||
>>> evaluate_formula("ceil(a / b)", 5, 2)
|
||||
3.0
|
||||
>>> evaluate_formula("(a>b)*b+(a<=b)*a", 5, 3)
|
||||
3.0
|
||||
>>> evaluate_formula("(a>b)*b+(a<=b)*a", 2, 3)
|
||||
2.0
|
||||
"""
|
||||
# 安全的数学函数白名单
|
||||
safe_dict = {
|
||||
# 基本运算
|
||||
'abs': abs,
|
||||
'pow': pow,
|
||||
'round': round,
|
||||
# 数学函数
|
||||
'ceil': math.ceil,
|
||||
'floor': math.floor,
|
||||
'sqrt': math.sqrt,
|
||||
'exp': math.exp,
|
||||
'log': math.log,
|
||||
'log10': math.log10,
|
||||
# 三角函数
|
||||
'sin': math.sin,
|
||||
'cos': math.cos,
|
||||
'tan': math.tan,
|
||||
'asin': math.asin,
|
||||
'acos': math.acos,
|
||||
'atan': math.atan,
|
||||
# 常量
|
||||
'pi': math.pi,
|
||||
'e': math.e,
|
||||
# 变量
|
||||
'a': float(a),
|
||||
'b': float(b),
|
||||
'c': float(c),
|
||||
'd': float(d),
|
||||
}
|
||||
|
||||
try:
|
||||
# 使用eval计算公式,限制可用的函数和变量
|
||||
result = eval(formula, {"__builtins__": {}}, safe_dict)
|
||||
return float(result)
|
||||
except Exception as e:
|
||||
raise ValueError(f"公式计算错误: {str(e)}")
|
||||
|
||||
|
||||
def ceil_value(value: float) -> int:
|
||||
"""向上取整"""
|
||||
return math.ceil(value)
|
||||
|
||||
|
||||
def floor_value(value: float) -> int:
|
||||
"""向下取整"""
|
||||
return math.floor(value)
|
||||
|
||||
|
||||
def round_value(value: float, decimals: int = 0) -> float:
|
||||
"""
|
||||
四舍五入
|
||||
|
||||
Args:
|
||||
value: 要取整的值
|
||||
decimals: 保留小数位数
|
||||
|
||||
Returns:
|
||||
四舍五入后的值
|
||||
"""
|
||||
return round(value, decimals)
|
||||
|
||||
|
||||
def power(base: float, exponent: float) -> float:
|
||||
"""计算幂运算"""
|
||||
return math.pow(base, exponent)
|
||||
|
||||
|
||||
def sqrt_value(value: float) -> float:
|
||||
"""计算平方根"""
|
||||
if value < 0:
|
||||
raise ValueError("不能对负数求平方根")
|
||||
return math.sqrt(value)
|
||||
|
||||
|
||||
def add(a: float, b: float) -> float:
|
||||
"""加法"""
|
||||
return a + b
|
||||
|
||||
|
||||
def subtract(a: float, b: float) -> float:
|
||||
"""减法"""
|
||||
return a - b
|
||||
|
||||
|
||||
def multiply(a: float, b: float) -> float:
|
||||
"""乘法"""
|
||||
return a * b
|
||||
|
||||
|
||||
def divide(a: float, b: float) -> float:
|
||||
"""除法"""
|
||||
if b == 0:
|
||||
raise ValueError("除数不能为零")
|
||||
return a / b
|
||||
+16
-1
@@ -1302,6 +1302,11 @@ class humanSegmentation:
|
||||
return mp.Image(image_format=image_format, data=numpy_image)
|
||||
|
||||
def parsing(self, image, confidence, method, crop_multi, mask_components, prompt=None, my_unique_id=None):
|
||||
if isinstance(mask_components, str):
|
||||
mask_components = [int(x) for x in mask_components.split(',') if x]
|
||||
else:
|
||||
mask_components = mask_components if mask_components else []
|
||||
|
||||
if method == 'selfie_multiclass_256x256':
|
||||
try:
|
||||
import mediapipe as mp
|
||||
@@ -1328,6 +1333,9 @@ class humanSegmentation:
|
||||
ret_images = []
|
||||
ret_masks = []
|
||||
|
||||
if len(mask_components) == 0:
|
||||
return (image, torch.zeros_like(image[:, :, :, 0:1]), torch.tensor([0,0,0,0]))
|
||||
|
||||
with mp.tasks.vision.ImageSegmenter.create_from_options(options) as segmenter:
|
||||
for img in image:
|
||||
_image = torch.unsqueeze(img, 0)
|
||||
@@ -1361,7 +1369,14 @@ class humanSegmentation:
|
||||
mask_arrays.append(mask_background_array)
|
||||
else:
|
||||
for i, mask in enumerate(masks):
|
||||
condition = np.stack((mask.numpy_view(),) * image_shape[-1], axis=-1) > confidence
|
||||
mask_2d = mask.numpy_view()
|
||||
if mask_2d.ndim == 3 and mask_2d.shape[2] == 1:
|
||||
mask_2d = mask_2d.squeeze(axis=2)
|
||||
elif mask_2d.ndim != 2:
|
||||
raise ValueError(f"Unexpected mask shape: {mask_2d.shape}")
|
||||
condition = np.stack((mask_2d,) * image_shape[-1], axis=-1) > confidence
|
||||
if condition.ndim == 4 and condition.shape[2] == 1:
|
||||
condition = condition.squeeze(2)
|
||||
mask_array = np.where(condition, mask_foreground_array, mask_background_array)
|
||||
mask_arrays.append(mask_array)
|
||||
# Merge our masks taking the maximum from each
|
||||
|
||||
+205
-13
@@ -7,6 +7,7 @@ from PIL.PngImagePlugin import PngInfo
|
||||
from ..libs.utils import AlwaysEqualProxy, ByPassTypeTuple, cleanGPUUsedForce, compare_revision
|
||||
from ..libs.cache import cache, update_cache, remove_cache
|
||||
from ..libs.log import log_node_info, log_node_warn
|
||||
from ..libs.math import evaluate_formula
|
||||
import numpy as np
|
||||
import time
|
||||
import os
|
||||
@@ -19,7 +20,7 @@ import folder_paths
|
||||
|
||||
DEFAULT_FLOW_NUM = 2
|
||||
MAX_FLOW_NUM = 20
|
||||
lazy_options = {"lazy": True} if compare_revision(2543) else {}
|
||||
lazy_options = {"lazy": True}
|
||||
|
||||
any_type = AlwaysEqualProxy("*")
|
||||
|
||||
@@ -628,6 +629,131 @@ class mathStringOperation:
|
||||
return (a.endswith(b),)
|
||||
|
||||
|
||||
class simpleMath:
|
||||
"""简单计算器节点,支持字符串数学公式计算"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"value": ("STRING", {
|
||||
"default": "",
|
||||
"placeholder": "输入数学公式,如: a + b, pow(a, 2), ceil(a / b), floor(a * b), round(a / b, 2)"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"a": (any_type,),
|
||||
"b": (any_type,),
|
||||
"c": (any_type,),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT","FLOAT", "BOOLEAN")
|
||||
RETURN_NAMES = ("int", "float", "boolean")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "EasyUse/Logic/Math"
|
||||
|
||||
def execute(self, value, a=0, b=0, c=0):
|
||||
"""
|
||||
执行公式计算
|
||||
|
||||
支持的运算:
|
||||
- 基本运算:+、-、*、/、**(幂)、%(取模)
|
||||
- 比较运算:>, <, >=, <=, ==, !=
|
||||
- 函数:abs, pow, round, ceil, floor, sqrt, exp, log, log10
|
||||
- 三角函数:sin, cos, tan, asin, acos, atan
|
||||
- 常量:pi, e
|
||||
- 变量:a, b, c
|
||||
|
||||
示例公式:
|
||||
- a + b + c
|
||||
- (a>b)*b+(a<=b)*a
|
||||
- pow(a, 2) + pow(b, 2)
|
||||
- ceil(a / b)
|
||||
- floor(a * b)
|
||||
- round(a / b, 2)
|
||||
- sqrt(a)
|
||||
"""
|
||||
try:
|
||||
result = evaluate_formula(value, a, b, c)
|
||||
result_int = int(result)
|
||||
result_bool = result_int != 0
|
||||
return (result_int, result, result_bool)
|
||||
except Exception as e:
|
||||
error_msg = f"计算错误: {str(e)}"
|
||||
log_node_warn(error_msg)
|
||||
# 返回默认值
|
||||
return (0, 0.0, False)
|
||||
|
||||
|
||||
class simpleMathDual:
|
||||
"""双公式计算器节点,支持两个独立的数学公式计算"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"value1": ("STRING", {
|
||||
"default": "",
|
||||
"placeholder": "输入数学公式1,如: a + b, pow(a, 2), ceil(a / b)"
|
||||
}),
|
||||
"value2": ("STRING", {
|
||||
"default": "",
|
||||
"placeholder": "输入数学公式2,如: c * d, sqrt(c), floor(d / 2)"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"a": (any_type,),
|
||||
"b": (any_type,),
|
||||
"c": (any_type,),
|
||||
"d": (any_type,),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "FLOAT", "INT", "FLOAT")
|
||||
RETURN_NAMES = ("int1", "float1", "int2", "float2")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "EasyUse/Logic/Math"
|
||||
|
||||
def execute(self, value1, value2, a=0, b=0, c=0, d=0):
|
||||
"""
|
||||
执行双公式计算
|
||||
|
||||
支持的运算:
|
||||
- 基本运算:+、-、*、/、**(幂)、%(取模)
|
||||
- 比较运算:>, <, >=, <=, ==, !=
|
||||
- 函数:abs, pow, round, ceil, floor, sqrt, exp, log, log10
|
||||
- 三角函数:sin, cos, tan, asin, acos, atan
|
||||
- 常量:pi, e
|
||||
- 变量:a, b, c, d
|
||||
|
||||
示例公式:
|
||||
- value1: a + b, value2: c + d
|
||||
- value1: (a>b)*b+(a<=b)*a, value2: pow(c, 2) + pow(d, 2)
|
||||
- value1: ceil(a / b), value2: floor(c * d)
|
||||
- value1: sqrt(a), value2: round(c / d, 2)
|
||||
"""
|
||||
try:
|
||||
result1 = evaluate_formula(value1, a, b, c, d)
|
||||
result1_int = int(result1)
|
||||
except Exception as e:
|
||||
error_msg = f"公式1计算错误: {str(e)}"
|
||||
log_node_warn(error_msg)
|
||||
result1 = 0.0
|
||||
result1_int = 0
|
||||
|
||||
try:
|
||||
result2 = evaluate_formula(value2, a, b, c, d)
|
||||
result2_int = int(result2)
|
||||
except Exception as e:
|
||||
error_msg = f"公式2计算错误: {str(e)}"
|
||||
log_node_warn(error_msg)
|
||||
result2 = 0.0
|
||||
result2_int = 0
|
||||
|
||||
return (result1_int, result1, result2_int, result2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------Flow----------------------------------------------------------------------#
|
||||
try:
|
||||
from comfy_execution.graph_utils import GraphBuilder, is_link
|
||||
@@ -1386,19 +1512,17 @@ class showAnything:
|
||||
values = []
|
||||
if "anything" in kwargs:
|
||||
for val in kwargs['anything']:
|
||||
try:
|
||||
if isinstance(val, str):
|
||||
values.append(val)
|
||||
# elif isinstance(val, list):
|
||||
# values = val
|
||||
elif isinstance(val, (int, float, bool)):
|
||||
values.append(str(val))
|
||||
else:
|
||||
val = json.dumps(val, indent=4)
|
||||
values.append(str(val))
|
||||
except Exception:
|
||||
if isinstance(val, str):
|
||||
values.append(val)
|
||||
elif isinstance(val, list) and len(val) <= 30:
|
||||
try:
|
||||
values = val
|
||||
except Exception:
|
||||
values.append(json.dumps(val, indent=4, ensure_ascii=False))
|
||||
elif isinstance(val, (int, float, bool)):
|
||||
values.append(str(val))
|
||||
pass
|
||||
else:
|
||||
values.append(json.dumps(val, indent=4, ensure_ascii=False))
|
||||
|
||||
if not extra_pnginfo:
|
||||
pass
|
||||
@@ -1444,6 +1568,64 @@ class showTensorShape:
|
||||
|
||||
return {"ui": {"text": shapes}}
|
||||
|
||||
class stringToIntList:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required":
|
||||
{
|
||||
"string" :("STRING", {"default": "1, 2, 3", "multiline": True}),
|
||||
}
|
||||
}
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ('INT',)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "EasyUse/Logic"
|
||||
|
||||
def execute(self, string):
|
||||
int_list = [int(x.strip()) for x in string.split(',')]
|
||||
return (int_list,)
|
||||
|
||||
class stringToFloatList:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required":
|
||||
{
|
||||
"string" :("STRING", {"default": "1, 2, 3", "multiline": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FLOAT",)
|
||||
RETURN_NAMES = ('FLOAT',)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "EasyUse/Logic"
|
||||
|
||||
def execute(self, string):
|
||||
float_list = [float(x.strip()) for x in string.split(',')]
|
||||
return (float_list,)
|
||||
|
||||
|
||||
class stringJoinLines:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required":
|
||||
{
|
||||
"string" :("STRING", {"default": "", "multiline": True}),
|
||||
"delimiter": ("STRING", {"default": " | "}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ('STRING',)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "EasyUse/Logic"
|
||||
|
||||
def execute(self, string, delimiter):
|
||||
# 将多行字符串按换行符分割成列表,去除空行和每行的首尾空格
|
||||
lines = [line.strip() for line in string.split('\n') if line.strip()]
|
||||
# 用指定的分隔符连接各行
|
||||
result = delimiter.join(lines)
|
||||
return (result,)
|
||||
|
||||
|
||||
class outputToList:
|
||||
@classmethod
|
||||
@@ -1730,6 +1912,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy mathString": mathStringOperation,
|
||||
"easy mathInt": mathIntOperation,
|
||||
"easy mathFloat": mathFloatOperation,
|
||||
"easy simpleMath": simpleMath,
|
||||
"easy simpleMathDual": simpleMathDual,
|
||||
"easy compare": Compare,
|
||||
"easy imageSwitch": imageSwitch,
|
||||
"easy textSwitch": textSwitch,
|
||||
@@ -1749,6 +1933,9 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy isNone": isNone,
|
||||
"easy isSDXL": isSDXL,
|
||||
"easy isFileExist": isFileExist,
|
||||
"easy stringToIntList": stringToIntList,
|
||||
"easy stringToFloatList": stringToFloatList,
|
||||
"easy stringJoinLines": stringJoinLines,
|
||||
"easy outputToList": outputToList,
|
||||
"easy pixels": pixels,
|
||||
"easy xyAny": xyAny,
|
||||
@@ -1775,6 +1962,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy mathString": "Math String",
|
||||
"easy mathInt": "Math Int",
|
||||
"easy mathFloat": "Math Float",
|
||||
"easy simpleMath": "Simple Math",
|
||||
"easy simpleMathDual": "Simple Math Dual",
|
||||
"easy imageSwitch": "Image Switch",
|
||||
"easy textSwitch": "Text Switch",
|
||||
"easy imageIndexSwitch": "Image Index Switch",
|
||||
@@ -1793,6 +1982,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy isNone": "Is None",
|
||||
"easy isSDXL": "Is SDXL",
|
||||
"easy isFileExist": "Is File Exist",
|
||||
"easy stringToIntList": "String to Int List",
|
||||
"easy stringToFloatList":"String to Float List",
|
||||
"easy stringJoinLines": "String Join Lines",
|
||||
"easy outputToList": "Output to List",
|
||||
"easy pixels": "Pixels W/H Norm",
|
||||
"easy xyAny": "XY Any",
|
||||
|
||||
+436
-294
@@ -1,157 +1,151 @@
|
||||
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 comfy_api.latest import io
|
||||
|
||||
|
||||
# 正面提示词
|
||||
class positivePrompt:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
class positivePrompt(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"positive": ("STRING", {"default": "", "multiline": True, "placeholder": "Positive"}),}
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy positive",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("positive", default="", multiline=True, placeholder="Positive"),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_positive", display_name="positive"),
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("positive",)
|
||||
FUNCTION = "main"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
@staticmethod
|
||||
def main(positive):
|
||||
return positive,
|
||||
@classmethod
|
||||
def execute(cls, positive):
|
||||
return io.NodeOutput(positive)
|
||||
|
||||
# 通配符提示词
|
||||
class wildcardsPrompt:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
class wildcardsPrompt(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def define_schema(cls):
|
||||
wildcard_list = get_wildcard_list()
|
||||
return {"required": {
|
||||
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support wildcard)"}),
|
||||
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
|
||||
"Select to add Wildcard": (["Select the Wildcard to add to the text"] + wildcard_list,),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}),
|
||||
"multiline_mode": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
return io.Schema(
|
||||
node_id="easy wildcards",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support wildcard)"),
|
||||
io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")),
|
||||
io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list),
|
||||
io.Int.Input("seed", default=0, min=0, max=MAX_SEED_NUM),
|
||||
io.Boolean.Input("multiline_mode", default=False),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_text", display_name="text", is_output_list=True),
|
||||
io.String.Output(id="populated_text", display_name="populated_text", is_output_list=True),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.extra_pnginfo,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("text", "populated_text")
|
||||
OUTPUT_IS_LIST = (True, True)
|
||||
FUNCTION = "main"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
prompt = kwargs["prompt"] if "prompt" in kwargs else None
|
||||
seed = kwargs["seed"]
|
||||
@classmethod
|
||||
def execute(cls, text, seed, multiline_mode, **kwargs):
|
||||
prompt = cls.hidden.prompt
|
||||
|
||||
# Clean loaded_objects
|
||||
if prompt:
|
||||
easyCache.update_loaded_objects(prompt)
|
||||
|
||||
text = kwargs['text']
|
||||
if "multiline_mode" in kwargs and kwargs["multiline_mode"]:
|
||||
if multiline_mode:
|
||||
populated_text = []
|
||||
_text = []
|
||||
text = text.split("\n")
|
||||
for t in text:
|
||||
text_lines = text.split("\n")
|
||||
for t in text_lines:
|
||||
_text.append(t)
|
||||
populated_text.append(process(t, seed))
|
||||
text = _text
|
||||
else:
|
||||
populated_text = [process(text, seed)]
|
||||
text = [text]
|
||||
return {"ui": {"value": [seed]}, "result": (text, populated_text)}
|
||||
return io.NodeOutput(text, populated_text, ui={"value": [seed]})
|
||||
|
||||
# 通配符提示词矩阵,会按顺序返回包含通配符的提示词所生成的所有可能
|
||||
class wildcardsPromptMatrix:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
class wildcardsPromptMatrix(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def define_schema(cls):
|
||||
wildcard_list = get_wildcard_list()
|
||||
return {"required": {
|
||||
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support Lora Block Weight and wildcard)"}),
|
||||
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
|
||||
"Select to add Wildcard": (["Select the Wildcard to add to the text"] + wildcard_list,),
|
||||
"offset": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM, "step": 1, "control_after_generate": True}),
|
||||
},
|
||||
"optional":{
|
||||
"output_limit": ("INT", {"default": 1, "min": -1, "step": 1, "tooltip": "Output All Probilities"})
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
return io.Schema(
|
||||
node_id="easy wildcardsMatrix",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support Lora Block Weight and wildcard)"),
|
||||
io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")),
|
||||
io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list),
|
||||
io.Int.Input("offset", default=0, min=0, max=MAX_SEED_NUM, step=1, control_after_generate=True),
|
||||
io.Int.Input("output_limit", default=1, min=-1, step=1, tooltip="Output All Probilities", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("populated_text", is_output_list=True),
|
||||
io.Int.Output("total"),
|
||||
io.Int.Output("factors", is_output_list=True),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.extra_pnginfo,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT", "INT")
|
||||
RETURN_NAMES = ("populated_text", "total", "factors")
|
||||
OUTPUT_IS_LIST = (True, False, True)
|
||||
FUNCTION = "main"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
prompt = kwargs["prompt"] if "prompt" in kwargs else None
|
||||
offset = kwargs["offset"]
|
||||
output_limit = kwargs.get("output_limit", 1)
|
||||
@classmethod
|
||||
def execute(cls, text, offset, output_limit=1, **kwargs):
|
||||
prompt = cls.hidden.prompt
|
||||
# Clean loaded_objects
|
||||
if prompt:
|
||||
easyCache.update_loaded_objects(prompt)
|
||||
|
||||
text = kwargs['text']
|
||||
p = WildcardProcessor(text)
|
||||
total = p.total()
|
||||
limit = total if output_limit > total or output_limit == -1 else output_limit
|
||||
offset = 0 if output_limit == -1 else offset
|
||||
populated_text = p.getmany(limit, offset) if output_limit != 1 else [p.getn(offset)]
|
||||
return {"ui": {"value": [offset]}, "result": (populated_text, p.total(), list(p.placeholder_choices.values()))}
|
||||
return io.NodeOutput(populated_text, p.total(), list(p.placeholder_choices.values()), ui={"value": [offset]})
|
||||
|
||||
# 负面提示词
|
||||
class negativePrompt:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
class negativePrompt(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"negative": ("STRING", {"default": "", "multiline": True, "placeholder": "Negative"}),}
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy negative",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("negative", default="", multiline=True, placeholder="Negative"),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_negative", display_name="negative"),
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("negative",)
|
||||
FUNCTION = "main"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
@staticmethod
|
||||
def main(negative):
|
||||
return negative,
|
||||
@classmethod
|
||||
def execute(cls, negative):
|
||||
return io.NodeOutput(negative)
|
||||
|
||||
# 风格提示词选择器
|
||||
class stylesPromptSelector:
|
||||
class stylesPromptSelector(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def define_schema(cls):
|
||||
styles = ["fooocus_styles"]
|
||||
styles_dir = FOOOCUS_STYLES_DIR
|
||||
for file_name in os.listdir(styles_dir):
|
||||
@@ -160,25 +154,28 @@ class stylesPromptSelector:
|
||||
if file_name != "fooocus_styles.json":
|
||||
styles.append(file_name.split(".")[0])
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"styles": (styles, {"default": "fooocus_styles"}),
|
||||
},
|
||||
"optional": {
|
||||
"positive": ("STRING", {"forceInput": True}),
|
||||
"negative": ("STRING", {"forceInput": True}),
|
||||
"select_styles": ("EASY_PROMPT_STYLES", {}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
return io.Schema(
|
||||
node_id="easy stylesSelector",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.Combo.Input("styles", options=styles, default="fooocus_styles"),
|
||||
io.String.Input("positive", default="", force_input=True, optional=True),
|
||||
io.String.Input("negative", default="", force_input=True, optional=True),
|
||||
io.Custom(io_type="EASY_PROMPT_STYLES").Input("select_styles", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_positive", display_name="positive"),
|
||||
io.String.Output(id="output_negative", display_name="negative"),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.extra_pnginfo,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING",)
|
||||
RETURN_NAMES = ("positive", "negative",)
|
||||
|
||||
CATEGORY = 'EasyUse/Prompt'
|
||||
FUNCTION = 'run'
|
||||
|
||||
def run(self, styles, positive='', negative='', select_styles=None, prompt=None, extra_pnginfo=None, my_unique_id=None):
|
||||
@classmethod
|
||||
def execute(cls, styles, positive='', negative='', select_styles=None, **kwargs):
|
||||
values = []
|
||||
all_styles = {}
|
||||
positive_prompt, negative_prompt = '', negative
|
||||
@@ -203,7 +200,7 @@ class stylesPromptSelector:
|
||||
|
||||
has_prompt = False
|
||||
if len(values) == 0:
|
||||
return (positive, negative)
|
||||
return io.NodeOutput(positive, negative)
|
||||
|
||||
for index, val in enumerate(values):
|
||||
if val not in all_styles:
|
||||
@@ -222,95 +219,101 @@ class stylesPromptSelector:
|
||||
if has_prompt == False and positive:
|
||||
positive_prompt = positive + positive_prompt + ', '
|
||||
|
||||
return (positive_prompt, negative_prompt)
|
||||
return io.NodeOutput(positive_prompt, negative_prompt)
|
||||
|
||||
#prompt
|
||||
class prompt:
|
||||
class prompt(io.ComfyNode):
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"text": ("STRING", {"default": "", "multiline": True, "placeholder": "Prompt"}),
|
||||
"prefix": (["Select the prefix add to the text"] + PROMPT_TEMPLATE["prefix"], {"default": "Select the prefix add to the text"}),
|
||||
"subject": (["👤Select the subject add to the text"] + PROMPT_TEMPLATE["subject"], {"default": "👤Select the subject add to the text"}),
|
||||
"action": (["🎬Select the action add to the text"] + PROMPT_TEMPLATE["action"], {"default": "🎬Select the action add to the text"}),
|
||||
"clothes": (["👚Select the clothes add to the text"] + PROMPT_TEMPLATE["clothes"], {"default": "👚Select the clothes add to the text"}),
|
||||
"environment": (["☀️Select the illumination environment add to the text"] + PROMPT_TEMPLATE["environment"], {"default": "☀️Select the illumination environment add to the text"}),
|
||||
"background": (["🎞️Select the background add to the text"] + PROMPT_TEMPLATE["background"], {"default": "🎞️Select the background add to the text"}),
|
||||
"nsfw": (["🔞Select the nsfw add to the text"] + PROMPT_TEMPLATE["nsfw"], {"default": "🔞️Select the nsfw add to the text"}),
|
||||
},"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy prompt",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("text", default="", multiline=True, placeholder="Prompt"),
|
||||
io.Combo.Input("prefix", options=["Select the prefix add to the text"] + PROMPT_TEMPLATE["prefix"], default="Select the prefix add to the text"),
|
||||
io.Combo.Input("subject", options=["👤Select the subject add to the text"] + PROMPT_TEMPLATE["subject"], default="👤Select the subject add to the text"),
|
||||
io.Combo.Input("action", options=["🎬Select the action add to the text"] + PROMPT_TEMPLATE["action"], default="🎬Select the action add to the text"),
|
||||
io.Combo.Input("clothes", options=["👚Select the clothes add to the text"] + PROMPT_TEMPLATE["clothes"], default="👚Select the clothes add to the text"),
|
||||
io.Combo.Input("environment", options=["☀️Select the illumination environment add to the text"] + PROMPT_TEMPLATE["environment"], default="☀️Select the illumination environment add to the text"),
|
||||
io.Combo.Input("background", options=["🎞️Select the background add to the text"] + PROMPT_TEMPLATE["background"], default="🎞️Select the background add to the text"),
|
||||
io.Combo.Input("nsfw", options=["🔞Select the nsfw add to the text"] + PROMPT_TEMPLATE["nsfw"], default="🔞️Select the nsfw add to the text"),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("prompt"),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.extra_pnginfo,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt",)
|
||||
FUNCTION = "doit"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def doit(self, *args, **kwargs):
|
||||
text = kwargs['text']
|
||||
return (text,)
|
||||
@classmethod
|
||||
def execute(cls, text, **kwargs):
|
||||
return io.NodeOutput(text)
|
||||
|
||||
#promptList
|
||||
class promptList:
|
||||
class promptList(io.ComfyNode):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {
|
||||
"prompt_1": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_2": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_3": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_4": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_5": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
"optional": {
|
||||
"optional_prompt_list": ("LIST",)
|
||||
}
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptList",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("prompt_1", multiline=True, default=""),
|
||||
io.String.Input("prompt_2", multiline=True, default=""),
|
||||
io.String.Input("prompt_3", multiline=True, default=""),
|
||||
io.String.Input("prompt_4", multiline=True, default=""),
|
||||
io.String.Input("prompt_5", multiline=True, default=""),
|
||||
io.Custom(io_type="LIST").Input("optional_prompt_list", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom(io_type="LIST").Output("prompt_list"),
|
||||
io.String.Output("prompt_strings", is_output_list=True),
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("LIST", "STRING")
|
||||
RETURN_NAMES = ("prompt_list", "prompt_strings")
|
||||
OUTPUT_IS_LIST = (False, True)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def run(self, **kwargs):
|
||||
@classmethod
|
||||
def execute(cls, prompt_1="", prompt_2="", prompt_3="", prompt_4="", prompt_5="", optional_prompt_list=None, **kwargs):
|
||||
prompts = []
|
||||
|
||||
if "optional_prompt_list" in kwargs:
|
||||
for l in kwargs["optional_prompt_list"]:
|
||||
if optional_prompt_list:
|
||||
for l in optional_prompt_list:
|
||||
prompts.append(l)
|
||||
|
||||
# Iterate over the received inputs in sorted order.
|
||||
for k in sorted(kwargs.keys()):
|
||||
v = kwargs[k]
|
||||
# Add individual prompts
|
||||
for p in [prompt_1, prompt_2, prompt_3, prompt_4, prompt_5]:
|
||||
if isinstance(p, str) and p != '':
|
||||
prompts.append(p)
|
||||
|
||||
# Only process string input ports.
|
||||
if isinstance(v, str) and v != '':
|
||||
prompts.append(v)
|
||||
|
||||
return (prompts, prompts)
|
||||
return io.NodeOutput(prompts, prompts)
|
||||
|
||||
#promptLine
|
||||
class promptLine:
|
||||
class promptLine(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {
|
||||
"prompt": ("STRING", {"multiline": True, "default": "text"}),
|
||||
"start_index": ("INT", {"default": 0, "min": 0, "max": 9999}),
|
||||
"max_rows": ("INT", {"default": 1000, "min": 1, "max": 9999}),
|
||||
"remove_empty_lines": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"hidden":{
|
||||
"workflow_prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"
|
||||
}
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptLine",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("prompt", multiline=True, default="text"),
|
||||
io.Int.Input("start_index", default=0, min=0, max=9999),
|
||||
io.Int.Input("max_rows", default=1000, min=1, max=9999),
|
||||
io.Boolean.Input("remove_empty_lines", default=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("STRING", is_output_list=True),
|
||||
io.Combo.Output("COMBO", is_output_list=True),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.unique_id,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", AlwaysEqualProxy('*'))
|
||||
RETURN_NAMES = ("STRING", "COMBO")
|
||||
OUTPUT_IS_LIST = (True, True)
|
||||
FUNCTION = "generate_strings"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def generate_strings(self, prompt, start_index, max_rows, remove_empty_lines=True, workflow_prompt=None, my_unique_id=None):
|
||||
@classmethod
|
||||
def execute(cls, prompt, start_index, max_rows, remove_empty_lines=True, **kwargs):
|
||||
lines = prompt.split('\n')
|
||||
|
||||
if remove_empty_lines:
|
||||
@@ -322,35 +325,40 @@ class promptLine:
|
||||
|
||||
rows = lines[start_index:end_index]
|
||||
|
||||
return (rows, rows)
|
||||
return io.NodeOutput(rows, rows)
|
||||
|
||||
import comfy.utils
|
||||
from server import PromptServer
|
||||
from ..libs.messages import MessageCancelled, Message
|
||||
any_type = AlwaysEqualProxy("*")
|
||||
class promptAwait:
|
||||
class promptAwait(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"now": (any_type,),
|
||||
"prompt": ("STRING", {"multiline": True, "default": "", "placeholder":"Enter a prompt or use voice to enter to text"}),
|
||||
"toolbar":("EASY_PROMPT_AWAIT_BAR",),
|
||||
},
|
||||
"optional":{
|
||||
"prev": (any_type,),
|
||||
},
|
||||
"hidden": {"workflow_prompt": "PROMPT", "my_unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptAwait",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.AnyType.Input("now"),
|
||||
io.String.Input("prompt", multiline=True, default="", placeholder="Enter a prompt or use voice to enter to text"),
|
||||
io.Custom(io_type="EASY_PROMPT_AWAIT_BAR").Input("toolbar"),
|
||||
io.AnyType.Input("prev", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.AnyType.Output(id="output", display_name="output"),
|
||||
io.String.Output(id="output_prompt", display_name="prompt"),
|
||||
io.Boolean.Output("continue"),
|
||||
io.Int.Output("seed"),
|
||||
],
|
||||
hidden=[
|
||||
io.Hidden.prompt,
|
||||
io.Hidden.unique_id,
|
||||
io.Hidden.extra_pnginfo,
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = (any_type, "STRING", "BOOLEAN", "INT")
|
||||
RETURN_NAMES = ("output", "prompt", "continue", "seed")
|
||||
FUNCTION = "await_select"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def await_select(self, now, prompt, toolbar, prev=None, workflow_prompt=None, my_unique_id=None, extra_pnginfo=None, **kwargs):
|
||||
id = my_unique_id
|
||||
@classmethod
|
||||
def execute(cls, now, prompt, toolbar, prev=None, **kwargs):
|
||||
id = cls.hidden.unique_id
|
||||
id = id.split('.')[len(id.split('.')) - 1] if "." in id else id
|
||||
if ":" in id:
|
||||
id = id.split(":")[0]
|
||||
@@ -365,60 +373,59 @@ class promptAwait:
|
||||
input = now if res['select'] == 'now' or prev is None else prev
|
||||
result = (input, res['prompt'], False if res['result'] == -1 else True, res['seed'] if res['unlock'] else res['last_seed'])
|
||||
pbar.update_absolute(100)
|
||||
return result
|
||||
return io.NodeOutput(*result)
|
||||
except MessageCancelled:
|
||||
pbar.update_absolute(100)
|
||||
raise comfy.model_management.InterruptProcessingException()
|
||||
|
||||
class promptConcat:
|
||||
class promptConcat(io.ComfyNode):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {},
|
||||
"optional": {
|
||||
"prompt1": ("STRING", {"multiline": False, "default": "", "forceInput": True}),
|
||||
"prompt2": ("STRING", {"multiline": False, "default": "", "forceInput": True}),
|
||||
"separator": ("STRING", {"multiline": False, "default": ""}),
|
||||
},
|
||||
}
|
||||
RETURN_TYPES = ("STRING", )
|
||||
RETURN_NAMES = ("prompt", )
|
||||
FUNCTION = "concat_text"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def concat_text(self, prompt1="", prompt2="", separator=""):
|
||||
|
||||
return (prompt1 + separator + prompt2,)
|
||||
|
||||
class promptReplace:
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptConcat",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("prompt1", multiline=False, default="", force_input=True, optional=True),
|
||||
io.String.Input("prompt2", multiline=False, default="", force_input=True, optional=True),
|
||||
io.String.Input("separator", multiline=False, default="", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("prompt"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
|
||||
},
|
||||
"optional": {
|
||||
"find1": ("STRING", {"multiline": False, "default": ""}),
|
||||
"replace1": ("STRING", {"multiline": False, "default": ""}),
|
||||
"find2": ("STRING", {"multiline": False, "default": ""}),
|
||||
"replace2": ("STRING", {"multiline": False, "default": ""}),
|
||||
"find3": ("STRING", {"multiline": False, "default": ""}),
|
||||
"replace3": ("STRING", {"multiline": False, "default": ""}),
|
||||
},
|
||||
}
|
||||
def execute(cls, prompt1="", prompt2="", separator=""):
|
||||
return io.NodeOutput(prompt1 + separator + prompt2)
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt",)
|
||||
FUNCTION = "replace_text"
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
class promptReplace(io.ComfyNode):
|
||||
|
||||
def replace_text(self, prompt, find1="", replace1="", find2="", replace2="", find3="", replace3=""):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy promptReplace",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.String.Input("prompt", multiline=True, default="", force_input=True),
|
||||
io.String.Input("find1", multiline=False, default="", optional=True),
|
||||
io.String.Input("replace1", multiline=False, default="", optional=True),
|
||||
io.String.Input("find2", multiline=False, default="", optional=True),
|
||||
io.String.Input("replace2", multiline=False, default="", optional=True),
|
||||
io.String.Input("find3", multiline=False, default="", optional=True),
|
||||
io.String.Input("replace3", multiline=False, default="", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output(id="output_prompt",display_name="prompt"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, prompt, find1="", replace1="", find2="", replace2="", find3="", replace3=""):
|
||||
prompt = prompt.replace(find1, replace1)
|
||||
prompt = prompt.replace(find2, replace2)
|
||||
prompt = prompt.replace(find3, replace3)
|
||||
|
||||
return (prompt,)
|
||||
return io.NodeOutput(prompt)
|
||||
|
||||
|
||||
# 肖像大师
|
||||
@@ -426,10 +433,10 @@ class promptReplace:
|
||||
# Version: 2.2
|
||||
# https://stefanoflore.it
|
||||
# https://ai-wiz.art
|
||||
class portraitMaster:
|
||||
class portraitMaster(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def define_schema(cls):
|
||||
max_float_value = 1.95
|
||||
prompt_path = os.path.join(RESOURCES_DIR, 'portrait_prompt.json')
|
||||
if not os.path.exists(prompt_path):
|
||||
@@ -441,50 +448,72 @@ class portraitMaster:
|
||||
del response, temp_prompt
|
||||
# Load local
|
||||
with open(prompt_path, 'r') as f:
|
||||
list = json.load(f)
|
||||
keys = [
|
||||
['shot', 'COMBO', {"key": "shot_list"}], ['shot_weight', 'FLOAT'],
|
||||
['gender', 'COMBO', {"default": "Woman", "key": "gender_list"}], ['age', 'INT', {"default": 30, "min": 18, "max": 90, "step": 1, "display": "slider"}],
|
||||
['nationality_1', 'COMBO', {"default": "Chinese", "key": "nationality_list"}], ['nationality_2', 'COMBO', {"key": "nationality_list"}], ['nationality_mix', 'FLOAT'],
|
||||
['body_type', 'COMBO', {"key": "body_type_list"}], ['body_type_weight', 'FLOAT'], ['model_pose', 'COMBO', {"key": "model_pose_list"}], ['eyes_color', 'COMBO', {"key": "eyes_color_list"}],
|
||||
['facial_expression', 'COMBO', {"key": "face_expression_list"}], ['facial_expression_weight', 'FLOAT'], ['face_shape', 'COMBO', {"key": "face_shape_list"}], ['face_shape_weight', 'FLOAT'], ['facial_asymmetry', 'FLOAT'],
|
||||
['hair_style', 'COMBO', {"key": "hair_style_list"}], ['hair_color', 'COMBO', {"key": "hair_color_list"}], ['disheveled', 'FLOAT'], ['beard', 'COMBO', {"key": "beard_list"}],
|
||||
['skin_details', 'FLOAT'], ['skin_pores', 'FLOAT'], ['dimples', 'FLOAT'], ['freckles', 'FLOAT'],
|
||||
['moles', 'FLOAT'], ['skin_imperfections', 'FLOAT'], ['skin_acne', 'FLOAT'], ['tanned_skin', 'FLOAT'],
|
||||
['eyes_details', 'FLOAT'], ['iris_details', 'FLOAT'], ['circular_iris', 'FLOAT'], ['circular_pupil', 'FLOAT'],
|
||||
['light_type', 'COMBO', {"key": "light_type_list"}], ['light_direction', 'COMBO', {"key": "light_direction_list"}], ['light_weight', 'FLOAT']
|
||||
]
|
||||
widgets = {}
|
||||
for i, obj in enumerate(keys):
|
||||
if obj[1] == 'COMBO':
|
||||
key = obj[2]['key'] if obj[2] and 'key' in obj[2] else obj[0]
|
||||
_list = list[key].copy()
|
||||
_list.insert(0, '-')
|
||||
widgets[obj[0]] = (_list, {**obj[2]})
|
||||
elif obj[1] == 'FLOAT':
|
||||
widgets[obj[0]] = ("FLOAT", {"default": 0, "step": 0.05, "min": 0, "max": max_float_value, "display": "slider",})
|
||||
elif obj[1] == 'INT':
|
||||
widgets[obj[0]] = (obj[1], obj[2])
|
||||
del list
|
||||
return {
|
||||
"required": {
|
||||
**widgets,
|
||||
"photorealism_improvement": (["enable", "disable"],),
|
||||
"prompt_start": ("STRING", {"multiline": True, "default": "raw photo, (realistic:1.5)"}),
|
||||
"prompt_additional": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_end": ("STRING", {"multiline": True, "default": ""}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
data = json.load(f)
|
||||
|
||||
inputs = []
|
||||
# Shot
|
||||
inputs.append(io.Combo.Input("shot", options=['-'] + data['shot_list']))
|
||||
inputs.append(io.Float.Input("shot_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Gender and age
|
||||
inputs.append(io.Combo.Input("gender", options=['-'] + data['gender_list'], default="Woman"))
|
||||
inputs.append(io.Int.Input("age", default=30, min=18, max=90, step=1, display_mode=io.NumberDisplay.slider))
|
||||
# Nationality
|
||||
inputs.append(io.Combo.Input("nationality_1", options=['-'] + data['nationality_list'], default="Chinese"))
|
||||
inputs.append(io.Combo.Input("nationality_2", options=['-'] + data['nationality_list']))
|
||||
inputs.append(io.Float.Input("nationality_mix", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Body
|
||||
inputs.append(io.Combo.Input("body_type", options=['-'] + data['body_type_list']))
|
||||
inputs.append(io.Float.Input("body_type_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Combo.Input("model_pose", options=['-'] + data['model_pose_list']))
|
||||
inputs.append(io.Combo.Input("eyes_color", options=['-'] + data['eyes_color_list']))
|
||||
# Face
|
||||
inputs.append(io.Combo.Input("facial_expression", options=['-'] + data['face_expression_list']))
|
||||
inputs.append(io.Float.Input("facial_expression_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Combo.Input("face_shape", options=['-'] + data['face_shape_list']))
|
||||
inputs.append(io.Float.Input("face_shape_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("facial_asymmetry", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Hair
|
||||
inputs.append(io.Combo.Input("hair_style", options=['-'] + data['hair_style_list']))
|
||||
inputs.append(io.Combo.Input("hair_color", options=['-'] + data['hair_color_list']))
|
||||
inputs.append(io.Float.Input("disheveled", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Combo.Input("beard", options=['-'] + data['beard_list']))
|
||||
# Skin details
|
||||
inputs.append(io.Float.Input("skin_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("skin_pores", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("dimples", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("freckles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("moles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("skin_imperfections", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("skin_acne", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("tanned_skin", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Eyes
|
||||
inputs.append(io.Float.Input("eyes_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("iris_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("circular_iris", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
inputs.append(io.Float.Input("circular_pupil", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Light
|
||||
inputs.append(io.Combo.Input("light_type", options=['-'] + data['light_type_list']))
|
||||
inputs.append(io.Combo.Input("light_direction", options=['-'] + data['light_direction_list']))
|
||||
inputs.append(io.Float.Input("light_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
|
||||
# Additional
|
||||
inputs.append(io.Combo.Input("photorealism_improvement", options=["enable", "disable"]))
|
||||
inputs.append(io.String.Input("prompt_start", multiline=True, default="raw photo, (realistic:1.5)"))
|
||||
inputs.append(io.String.Input("prompt_additional", multiline=True, default=""))
|
||||
inputs.append(io.String.Input("prompt_end", multiline=True, default=""))
|
||||
inputs.append(io.String.Input("negative_prompt", multiline=True, default=""))
|
||||
|
||||
return io.Schema(
|
||||
node_id="easy portraitMaster",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=inputs,
|
||||
outputs=[
|
||||
io.String.Output("positive"),
|
||||
io.String.Output("negative"),
|
||||
],
|
||||
)
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING",)
|
||||
RETURN_NAMES = ("positive", "negative",)
|
||||
|
||||
FUNCTION = "pm"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def pm(self, shot="-", shot_weight=1, gender="-", body_type="-", body_type_weight=0, eyes_color="-",
|
||||
@classmethod
|
||||
def execute(cls, shot="-", shot_weight=1, gender="-", body_type="-", body_type_weight=0, eyes_color="-",
|
||||
facial_expression="-", facial_expression_weight=0, face_shape="-", face_shape_weight=0,
|
||||
nationality_1="-", nationality_2="-", nationality_mix=0.5, age=30, hair_style="-", hair_color="-",
|
||||
disheveled=0, dimples=0, freckles=0, skin_pores=0, skin_details=0, moles=0, skin_imperfections=0,
|
||||
@@ -614,7 +643,118 @@ class portraitMaster:
|
||||
|
||||
log_node_info("Portrait Master as generate the prompt:", prompt)
|
||||
|
||||
return (prompt, negative_prompt,)
|
||||
return io.NodeOutput(prompt, negative_prompt)
|
||||
|
||||
# 多角度
|
||||
class multiAngle(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="easy multiAngle",
|
||||
category="EasyUse/Prompt",
|
||||
inputs=[
|
||||
io.Custom(io_type="EASY_MULTI_ANGLE").Input("multi_angle", optional=True),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("prompt", is_output_list=True),
|
||||
io.Custom(io_type="EASY_MULTI_ANGLE").Output("params"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, multi_angle=None, **kwargs):
|
||||
if multi_angle is None:
|
||||
return io.NodeOutput([""])
|
||||
|
||||
if isinstance(multi_angle, str):
|
||||
try:
|
||||
multi_angle = json.loads(multi_angle)
|
||||
except:
|
||||
raise Exception(f"Invalid multi angle: {multi_angle}")
|
||||
|
||||
prompts = []
|
||||
for angle_data in multi_angle:
|
||||
rotate = angle_data.get("rotate", 0)
|
||||
vertical = angle_data.get("vertical", 0)
|
||||
zoom = angle_data.get("zoom", 5)
|
||||
add_angle_prompt = angle_data.get("add_angle_prompt", True)
|
||||
|
||||
# Validate input ranges
|
||||
rotate = max(0, min(360, int(rotate)))
|
||||
vertical = max(-90, min(90, int(vertical)))
|
||||
zoom = max(0.0, min(10.0, float(zoom)))
|
||||
|
||||
h_angle = rotate % 360
|
||||
|
||||
# Horizontal direction mapping
|
||||
h_suffix = "" if add_angle_prompt else " quarter"
|
||||
if h_angle < 22.5 or h_angle >= 337.5: h_direction = "front view"
|
||||
elif h_angle < 67.5: h_direction = f"front-right{h_suffix} view"
|
||||
elif h_angle < 112.5: h_direction = "right side view"
|
||||
elif h_angle < 157.5: h_direction = f"back-right{h_suffix} view"
|
||||
elif h_angle < 202.5: h_direction = "back view"
|
||||
elif h_angle < 247.5: h_direction = f"back-left{h_suffix} view"
|
||||
elif h_angle < 292.5: h_direction = "left side view"
|
||||
else: h_direction = f"front-left{h_suffix} view"
|
||||
|
||||
# Vertical direction mapping
|
||||
if add_angle_prompt:
|
||||
if vertical == -90:
|
||||
v_direction = "bottom-looking-up perspective, extreme worm's eye view, focus subject bottom"
|
||||
elif vertical < -75:
|
||||
v_direction = "bottom-looking-up perspective, extreme worm's eye view"
|
||||
elif vertical < -45:
|
||||
v_direction = "ultra-low angle"
|
||||
elif vertical < -15:
|
||||
v_direction = "low angle"
|
||||
elif vertical < 15:
|
||||
v_direction = "eye level"
|
||||
elif vertical < 45:
|
||||
v_direction = "high angle"
|
||||
elif vertical < 75:
|
||||
v_direction = "bird's eye view"
|
||||
elif vertical < 90:
|
||||
v_direction = "top-down perspective, looking straight down at the top of the subject"
|
||||
else:
|
||||
v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head"
|
||||
else:
|
||||
if vertical < -15:
|
||||
v_direction = "low-angle shot"
|
||||
elif vertical < 15:
|
||||
v_direction = "eye-level shot"
|
||||
elif vertical < 45:
|
||||
v_direction = "elevated shot"
|
||||
elif vertical < 75:
|
||||
v_direction = "high-angle shot"
|
||||
elif vertical < 90:
|
||||
v_direction = "top-down perspective, looking straight down at the top of the subject"
|
||||
else:
|
||||
v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head"
|
||||
|
||||
# Distance/zoom mapping
|
||||
if add_angle_prompt:
|
||||
if zoom < 2: distance = "extreme wide shot"
|
||||
elif zoom < 4: distance = "wide shot"
|
||||
elif zoom < 6: distance = "medium shot"
|
||||
elif zoom < 8: distance = "close-up"
|
||||
else: distance = "extreme close-up"
|
||||
else:
|
||||
if zoom < 2: distance = "extreme wide shot"
|
||||
elif zoom < 4: distance = "wide shot"
|
||||
elif zoom < 6: distance = "medium shot"
|
||||
elif zoom < 8: distance = "close-up"
|
||||
else: distance = "extreme close-up"
|
||||
|
||||
# Build prompt
|
||||
if add_angle_prompt:
|
||||
prompt = f"{h_direction}, {v_direction}, {distance} (horizontal: {rotate}, vertical: {vertical}, zoom: {zoom:.1f})"
|
||||
else:
|
||||
prompt = f"{h_direction} {v_direction} {distance}"
|
||||
|
||||
prompts.append(prompt)
|
||||
|
||||
return io.NodeOutput(prompts, multi_angle)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -630,6 +770,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy promptReplace": promptReplace,
|
||||
"easy stylesSelector": stylesPromptSelector,
|
||||
"easy portraitMaster": portraitMaster,
|
||||
"easy multiAngle": multiAngle,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -645,4 +786,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy promptReplace": "PromptReplace",
|
||||
"easy stylesSelector": "Styles Selector",
|
||||
"easy portraitMaster": "Portrait Master",
|
||||
"easy multiAngle": "Multi Angle",
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
[project]
|
||||
name = "comfyui-easy-use"
|
||||
description = "To enhance the usability of ComfyUI, optimizations and integrations have been implemented for several commonly used nodes."
|
||||
version = "1.3.5"
|
||||
version = "1.3.6"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["diffusers", "accelerate", "clip_interrogator>=0.6.0", "sentencepiece", "lark", "onnxruntime", "spandrel", "opencv-python-headless", "matplotlib", "peft"]
|
||||
dependencies = ["diffusers", "accelerate", "clip_interrogator", "sentencepiece", "lark", "onnxruntime", "spandrel", "opencv-python-headless", "matplotlib", "peft"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/yolain/ComfyUI-Easy-Use"
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# 开发人员使用(请勿运行)
|
||||
# 将 https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Translation 的翻译文件转换格式以适配 ComfyUI locales
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
old_json_path = 'ComfyUI-Easy-Use.json'
|
||||
root_path = pathlib.Path(__file__).parent.parent
|
||||
new_json_path = os.path.join(root_path,'locales/zh/nodeDefs.json')
|
||||
|
||||
def transform_dict(data):
|
||||
new_dict = {}
|
||||
for k, v in data.items():
|
||||
new_dict[k] = {
|
||||
"display_name": "",
|
||||
"inputs": {}
|
||||
}
|
||||
if isinstance(v, dict):
|
||||
for key, value in v.items():
|
||||
if key == 'title':
|
||||
new_dict[k]['display_name'] = value
|
||||
elif key in ['inputs','widgets']:
|
||||
for _key, _value in value.items():
|
||||
new_dict[k]['inputs'] = {
|
||||
**new_dict[k]['inputs'],
|
||||
_key: {"name": _value}
|
||||
}
|
||||
elif key == 'outputs':
|
||||
if not new_dict[k].get('outputs'):
|
||||
new_dict[k]['outputs'] = {}
|
||||
for idx, (out_key, out_value) in enumerate(value.items()):
|
||||
new_dict[k]['outputs'][idx] = {"name": out_value}
|
||||
return new_dict
|
||||
|
||||
def main():
|
||||
|
||||
# 读取原始JSON文件
|
||||
with open(old_json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 转换数据
|
||||
transformed_data = transform_dict(data)
|
||||
|
||||
# 写入新的JSON文件
|
||||
with open(new_json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(transformed_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
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
+1
-1
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
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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user