Compare commits

..
16 Commits
Author SHA1 Message Date
yolain b5e31ef12a Bump Version 2026-01-23 14:18:34 +08:00
yolain 21b3c15040 Fix #946 2026-01-23 14:04:47 +08:00
yolain 5dfcbcf51d Fix custom widgets to support subgraph and Nodes 2.0 #942 2026-01-17 19:27:14 +08:00
yolain 070001b36b latest commit supplemental fix #939 2026-01-13 18:48:50 +08:00
yolain 6b4c89adc4 prompt.py is compatible with comfyui version <= 0.7.0 #939 2026-01-13 15:46:48 +08:00
yolain 32ad26f0e1 Add Invert rotate mode (#940)
* Remove the degree restriction on the vertical viewing angle

* Modify multi-perspective prompt

* Add Invert rotate mode
2026-01-13 14:14:50 +08:00
yolain d9c2072a2d Add Hollow Mode to easy multiAngle (#936)
- Global control to enable or disable angle prompts
- Added `Hollow Mode` for more intuitive visualization
- Removed label quantity limit; now supports unlimited additions
- Label addition button will copy parameters from the selected page
- Double-clicking any face of the cube quickly switches camera angles for easier operation

- 全局控制是否添加角度提示词
- 新增了`镂空模式`,可更直观地展示
- 去除标签限制个数,可添加无数个
- 标签添加按钮将复制选中页的参数
- 双击正方体的每一面可以快速切换摄像机角度,便于操作
2026-01-11 15:51:14 +08:00
yolain e94405e610 Fix easy multiAngle styles on light theme 2026-01-10 18:22:18 +08:00
yolain 03d5a4cf12 Update easy multiAngle frontend 2026-01-10 17:44:15 +08:00
yolain ad43ed3154 Add easy multiAngle for qwen 2511 multi lora 2026-01-10 17:33:43 +08:00
yolain 5cc1f8535a Convert prompt.py to V3 Schema 2026-01-10 13:20:48 +08:00
yolain 9f42ead9db Fix humanSegmentation error 2026-01-05 15:20:41 +08:00
yolain 23d9c365bd Add stringJoinLines 2025-12-30 13:47:07 +08:00
yolain 7a17ad010d Add stringToIntList and SimpleMath 2025-12-30 13:05:38 +08:00
yolain 3b38a5ae60 Fix lazy options 2025-12-30 12:25:43 +08:00
yolain 3b224fbccd update __init__.py 2025-12-28 14:51:23 +08:00
30 changed files with 2665 additions and 397 deletions
@@ -0,0 +1,605 @@
---
applyTo: "**/*.py"
description: "ComfyUI v3 Node Examples"
---
# ComfyUI v3 Node Examples
Real-world examples of v3 nodes demonstrating various features and patterns.
## Basic Examples
### Simple Image Processor
```python
from comfy_api.latest import io, ui
import torch
class ImageInvertV3(io.ComfyNode):
"""Simple node that inverts image colors."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageInvert_v3",
display_name="Invert Image",
category="image/filters",
description="Inverts the colors of an image",
inputs=[
io.Image.Input("image", tooltip="Image to invert")
],
outputs=[
io.Image.Output("inverted", tooltip="Inverted image")
]
)
@classmethod
def execute(cls, image):
# Invert: 1.0 - image
inverted = 1.0 - image
return io.NodeOutput(inverted, ui=ui.PreviewImage(inverted))
```
### Math Operations
```python
class MathOperationV3(io.ComfyNode):
"""Performs math operations on two values."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="MathOperation_v3",
display_name="Math Operation",
category="utils/math",
inputs=[
io.Float.Input("a", default=0.0),
io.Float.Input("b", default=0.0),
io.Combo.Input("operation",
options=["add", "subtract", "multiply", "divide", "power"],
default="add"
)
],
outputs=[
io.Float.Output("result")
]
)
@classmethod
def execute(cls, a, b, operation):
operations = {
"add": a + b,
"subtract": a - b,
"multiply": a * b,
"divide": a / b if b != 0 else 0,
"power": a ** b
}
result = operations[operation]
return io.NodeOutput(result)
```
## Async Examples
### API Integration
```python
import aiohttp
class TextGeneratorV3(io.ComfyNode):
"""Generates text using external API."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="TextGenerator_v3",
display_name="AI Text Generator",
category="text/generation",
inputs=[
io.String.Input("prompt", multiline=True),
io.String.Input("api_url", default="http://localhost:11434/api/generate"),
io.String.Input("model", default="llama2"),
io.Float.Input("temperature", default=0.7, min=0.0, max=2.0)
],
outputs=[
io.String.Output("generated_text")
]
)
@classmethod
async def execute(cls, prompt, api_url, model, temperature):
async with aiohttp.ClientSession() as session:
payload = {
"model": model,
"prompt": prompt,
"temperature": temperature,
"stream": False
}
async with session.post(api_url, json=payload) as response:
if response.status == 200:
data = await response.json()
text = data.get("response", "")
return io.NodeOutput(text)
else:
raise RuntimeError(f"API error: {response.status}")
```
### Batch Processing with Progress
```python
from comfy.utils import ProgressBar
import asyncio
class BatchImageProcessorV3(io.ComfyNode):
"""Processes images in batch with progress tracking."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="BatchImageProcessor_v3",
display_name="Batch Image Processor",
category="image/batch",
inputs=[
io.Image.Input("images"),
io.Float.Input("process_time", default=0.1, min=0.01, max=1.0,
tooltip="Simulated processing time per image")
],
outputs=[
io.Image.Output("processed")
],
hidden=[io.Hidden.unique_id]
)
@classmethod
async def execute(cls, images, process_time, **kwargs):
batch_size = images.shape[0]
pbar = ProgressBar(batch_size, node_id=cls.hidden.unique_id)
processed = []
for i in range(batch_size):
# Simulate async processing
await asyncio.sleep(process_time)
# Example: Apply blur
import torch.nn.functional as F
blurred = F.gaussian_blur(images[i:i+1], kernel_size=5)
processed.append(blurred)
pbar.update(1)
result = torch.cat(processed, dim=0)
return io.NodeOutput(result, ui=ui.PreviewImage(result))
```
## Advanced Examples
### Model Loader with Resources
```python
import folder_paths
import comfy.utils
import comfy.sd
class CheckpointLoaderV3(io.ComfyNode):
"""Loads checkpoint models with caching."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CheckpointLoader_v3",
display_name="Load Checkpoint",
category="loaders",
inputs=[
io.Combo.Input("ckpt_name",
options=folder_paths.get_filename_list("checkpoints"),
tooltip="Select checkpoint to load"
)
],
outputs=[
io.Model.Output("model"),
io.Clip.Output("clip"),
io.Vae.Output("vae")
]
)
@classmethod
def execute(cls, ckpt_name):
# Use resource caching
ckpt = cls.resources.get(
resources.TorchDictFolderFilename("checkpoints", ckpt_name)
)
# Load components
model, clip, vae = comfy.sd.load_checkpoint_guess_config(
ckpt,
embedding_directory=folder_paths.get_folder_paths("embeddings")
)
return io.NodeOutput(model, clip, vae)
```
### State Management Example
```python
class IterativeRefinerV3(io.ComfyNode):
"""Refines images iteratively with state tracking."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="IterativeRefiner_v3",
display_name="Iterative Refiner",
category="image/processing",
inputs=[
io.Image.Input("image"),
io.Int.Input("iterations", default=3, min=1, max=10),
io.Boolean.Input("reset", default=False,
tooltip="Reset refinement history")
],
outputs=[
io.Image.Output("refined"),
io.Int.Output("total_iterations")
]
)
@classmethod
def execute(cls, image, iterations, reset):
# Initialize or reset state
if reset or cls.state.history is None:
cls.state.history = []
cls.state.total_iterations = 0
# Get last refined image or use input
current = cls.state.history[-1] if cls.state.history else image
# Iterative refinement
for i in range(iterations):
# Example: Progressive sharpening
import torch.nn.functional as F
kernel = torch.tensor([[-1,-1,-1],
[-1, 9,-1],
[-1,-1,-1]], dtype=torch.float32)
kernel = kernel.view(1, 1, 3, 3)
kernel = kernel.repeat(current.shape[-1], 1, 1, 1)
current = current.permute(0, 3, 1, 2)
sharpened = F.conv2d(current, kernel, padding=1, groups=current.shape[1])
current = sharpened.permute(0, 2, 3, 1)
current = torch.clamp(current, 0, 1)
# Update state
cls.state.history.append(current)
cls.state.total_iterations += iterations
# Keep history size manageable
if len(cls.state.history) > 10:
cls.state.history.pop(0)
return io.NodeOutput(
current,
cls.state.total_iterations,
ui=ui.PreviewImage(current)
)
```
### Dynamic Inputs Example
```python
class ImageBlenderV3(io.ComfyNode):
"""Blends multiple images with weights."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ImageBlender_v3",
display_name="Image Blender",
category="image/blend",
inputs=[
io.AutoGrowDynamicInput("images",
template_input=io.Image.Input("image"),
min=2,
max=8
),
io.Combo.Input("mode",
options=["average", "weighted", "max", "min"],
default="average"
)
],
outputs=[
io.Image.Output("blended")
]
)
@classmethod
def execute(cls, mode, **kwargs):
# Collect all image inputs
images = []
for key, value in sorted(kwargs.items()):
if key.startswith("image"):
images.append(value)
if not images:
raise ValueError("No images provided")
# Stack images
stacked = torch.stack(images, dim=0)
# Blend based on mode
if mode == "average":
blended = torch.mean(stacked, dim=0)
elif mode == "weighted":
# Simple linear weighting
weights = torch.linspace(1, 0.1, len(images))
weights = weights / weights.sum()
weights = weights.view(-1, 1, 1, 1, 1)
blended = (stacked * weights).sum(dim=0)
elif mode == "max":
blended = torch.max(stacked, dim=0)[0]
elif mode == "min":
blended = torch.min(stacked, dim=0)[0]
return io.NodeOutput(blended, ui=ui.PreviewImage(blended))
```
### Multi-Type Input Example
```python
class UniversalInverterV3(io.ComfyNode):
"""Inverts images, masks, or conditioning."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="UniversalInverter_v3",
display_name="Universal Inverter",
category="utils/invert",
inputs=[
io.MultiType.Input("input",
types=[io.Image, io.Mask, io.Conditioning]
),
io.Float.Input("strength", default=1.0, min=0.0, max=1.0)
],
outputs=[
io.MultiType.Output("inverted",
types=[io.Image, io.Mask, io.Conditioning]
)
]
)
@classmethod
def execute(cls, input, strength):
# Detect input type and process accordingly
if isinstance(input, torch.Tensor):
# Image or Mask
if input.dim() == 4: # Image [B,H,W,C]
inverted = 1.0 - input
inverted = input + (inverted - input) * strength
return io.NodeOutput(inverted, ui=ui.PreviewImage(inverted))
else: # Mask [H,W] or [B,H,W]
inverted = 1.0 - input
inverted = input + (inverted - input) * strength
return io.NodeOutput(inverted, ui=ui.PreviewMask(inverted))
elif isinstance(input, list): # Conditioning
# Invert conditioning strength
inverted = []
for cond, data in input:
new_data = data.copy()
if 'strength' in new_data:
new_data['strength'] = 1.0 - new_data['strength']
inverted.append((cond, new_data))
return io.NodeOutput(inverted)
else:
raise ValueError(f"Unsupported input type: {type(input)}")
```
### Custom Type Example
```python
class CustomDataProcessorV3(io.ComfyNode):
"""Processes custom data types."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="CustomDataProcessor_v3",
display_name="Custom Data Processor",
category="utils/custom",
inputs=[
io.Custom(io_type="MY_CUSTOM_TYPE").Input("custom_data",,
tooltip="Custom data type input"
),
io.Float.Input("scale", default=1.0, min=0.1, max=10.0)
],
outputs=[
io.Custom(io_type="MY_CUSTOM_TYPE").Output("processed_data",
tooltip="Processed custom data"
)
]
)
@classmethod
def execute(cls, custom_data, scale):
# Process custom data type
# Assuming custom_data is a dict with 'value' and 'metadata'
processed = {
'value': custom_data.get('value', 0) * scale,
'metadata': custom_data.get('metadata', {}),
'processed': True
}
return io.NodeOutput(processed)
```
## Process Isolation Example
### Node with Specific Dependencies
```python
# manifest.yaml
"""
name: scientific_processor
version: 1.0.0
dependencies:
- numpy==1.24.0 # Specific older version needed
- scipy==1.10.0
- scikit-image==0.20.0
isolated: true
share_torch: true
"""
# __init__.py
from comfy_api.latest import io, io.ComfyNode, io.Schema
import numpy as np
from skimage import filters
class ScientificProcessorV3(io.ComfyNode):
"""Image processing with scientific libraries."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ScientificProcessor_v3",
display_name="Scientific Processor",
category="image/scientific",
inputs=[
io.Image.Input("image"),
io.Combo.Input("filter_type",
options=["gaussian", "sobel", "laplacian", "butterworth"],
default="gaussian"
),
io.Float.Input("sigma", default=1.0, min=0.1, max=10.0)
],
outputs=[
io.Image.Output("filtered")
]
)
@classmethod
def execute(cls, image, filter_type, sigma):
# Convert to numpy
img_np = image.cpu().numpy()
batch_size = img_np.shape[0]
results = []
for i in range(batch_size):
img = img_np[i]
if filter_type == "gaussian":
filtered = filters.gaussian(img, sigma=sigma, channel_axis=-1)
elif filter_type == "sobel":
gray = np.mean(img, axis=-1)
filtered = filters.sobel(gray)
filtered = np.stack([filtered]*3, axis=-1)
elif filter_type == "laplacian":
gray = np.mean(img, axis=-1)
filtered = filters.laplace(gray)
filtered = np.stack([filtered]*3, axis=-1)
elif filter_type == "butterworth":
# Frequency domain filtering
for c in range(3):
channel = img[:,:,c]
fft = np.fft.fft2(channel)
fft_shift = np.fft.fftshift(fft)
# Apply Butterworth filter
H = 1 / (1 + (D/sigma)**4) # Simplified
filtered_fft = fft_shift * H
filtered[:,:,c] = np.real(np.fft.ifft2(np.fft.ifftshift(filtered_fft)))
results.append(filtered)
# Convert back to tensor
result = torch.from_numpy(np.stack(results)).float()
return io.NodeOutput(result, ui=ui.PreviewImage(result))
# Entry point for pyisolate
from pyisolate import ExtensionBase
class ScientificExtension(ExtensionBase):
def on_module_loaded(self, module):
self.nodes = {
"ScientificProcessor_v3": ScientificProcessorV3
}
def create_extension():
return ScientificExtension()
```
## Complete Workflow Example
```python
class TextToImageWorkflowV3(io.ComfyNode):
"""Complete text-to-image workflow in one node."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="TextToImageWorkflow_v3",
display_name="Text to Image Workflow",
category="workflows",
description="All-in-one text to image generation",
inputs=[
io.String.Input("positive_prompt", multiline=True),
io.String.Input("negative_prompt", multiline=True, default=""),
io.Model.Input("model"),
io.Clip.Input("clip"),
io.Vae.Input("vae"),
io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff),
io.Int.Input("steps", default=20, min=1, max=150),
io.Float.Input("cfg", default=7.0, min=0.0, max=30.0),
io.Combo.Input("sampler_name",
options=comfy.samplers.KSampler.SAMPLERS,
default="euler"
),
io.Combo.Input("scheduler",
options=comfy.samplers.KSampler.SCHEDULERS,
default="normal"
),
io.Int.Input("width", default=1024, min=64, max=8192, step=8),
io.Int.Input("height", default=1024, min=64, max=8192, step=8),
io.Int.Input("batch_size", default=1, min=1, max=64)
],
outputs=[
io.Image.Output("images", is_output_list=True),
io.Latent.Output("latents")
],
is_output_node=True
)
@classmethod
async def execute(cls, positive_prompt, negative_prompt, model, clip, vae,
seed, steps, cfg, sampler_name, scheduler,
width, height, batch_size):
import comfy.samplers
# Encode prompts
positive_cond = clip.encode_from_text(positive_prompt)
negative_cond = clip.encode_from_text(negative_prompt)
# Create empty latent
latent = torch.zeros([batch_size, 4, height // 8, width // 8])
# Set up sampler
sampler = comfy.samplers.KSampler(
model, steps, cfg, sampler_name, scheduler,
positive_cond, negative_cond, latent,
denoise=1.0, seed=seed
)
# Sample with progress callback
def callback(step, x0, x, total_steps):
# Could update progress here
pass
samples = sampler.sample(latent, callback=callback)
# Decode latents
images = vae.decode(samples["samples"])
return io.NodeOutput(
images,
samples,
ui=ui.PreviewImage(images)
)
```
@@ -0,0 +1,530 @@
---
applyTo: "**/*.py"
description: "ComfyUI v3 Migration Guide"
---
# ComfyUI v3 Migration Guide
This guide helps developers migrate existing v1 nodes to the new v3 schema and take advantage of async execution and process isolation.
## Quick Start: The Core Changes
1. **Inherit from `io.ComfyNode`**: Your node class now subclasses `io.ComfyNode`.
2. **Use `define_schema`**: All metadata (`INPUT_TYPES`, `CATEGORY`, etc.) moves into a single `@classmethod def define_schema(cls)` that returns an `io.Schema` object.
3. **Use `execute`**: The main logic function is now always a `@classmethod def execute(cls, ...)` method.
4. **Use Typed I/O**: Inputs and outputs are now strongly-typed objects from the `io` module (e.g., `io.Image.Input(...)`).
5. **Return `NodeOutput`**: The `execute` method must return an `io.NodeOutput` instance.
6. **Use `NODES_LIST`**: Node registration is done by adding the class to a `NODES_LIST` at the end of the file, replacing `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS`.
## Step-by-Step Migration
### Step 1: Class Definition and Schema
**V1:**
```python
class Canny:
CATEGORY = "image/preprocessors"
FUNCTION = "detect_edge"
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image": ("IMAGE",),
"low_threshold": ("FLOAT", {"default": 0.4}),
"high_threshold": ("FLOAT", {"default": 0.8}),
}}
def detect_edge(self, image, low_threshold, high_threshold):
# ... logic ...
return (img_out,)
NODE_CLASS_MAPPINGS = {"Canny": Canny}
```
**V3:**
```python
from comfy_api.latest import io
class Canny(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="Canny_V3",
category="image/preprocessors",
inputs=[
io.Image.Input("image"),
io.Float.Input("low_threshold", default=0.4),
io.Float.Input("high_threshold", default=0.8),
],
outputs=[io.Image.Output()],
)
@classmethod
def execute(cls, image, low_threshold, high_threshold):
# ... logic ...
return io.NodeOutput(img_out)
NODES_LIST = [Canny]
```
### Step 2: Naming and Registration (`node_id`, `display_name`, `NODES_LIST`)
This is a critical step for ensuring your V3 node coexists with or replaces the V1 version correctly.
1. **Remove Old Mappings**: Delete the `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` dictionaries.
2. **Create `NODES_LIST`**: Create a new list called `NODES_LIST` and add your V3 class to it.
3. **Set `node_id`**: The `node_id` in `Schema` **must** be the key from the old `NODE_CLASS_MAPPINGS`.
4. **Set `display_name` (Conditionally)**:
- Check if a key existed in the old `NODE_DISPLAY_NAME_MAPPINGS`.
- **If yes**: Set `display_name` to that value.
- **If no**: **Omit** the `display_name` parameter from `Schema` entirely.
**Example:**
**V1 Registration:**
```python
NODE_CLASS_MAPPINGS = {
"APG": APG,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"APG": "Adaptive Projected Guidance",
}
```
**V3 `define_schema`:**
```python
@classmethod
def define_schema(cls):
return io.Schema(
node_id="APG_V3", # From MAPPINGS key + "_V3"
display_name="Adaptive Projected Guidance _V3", # From DISPLAY MAPPINGS value + " _V3"
# ... other parameters
)
NODES_LIST = [APG] # ... at end of file
```
### Step 3: Converting I/O
| V1 Type (`string`) | V3 Class (`io.<Type>`) | Common `Input()` Options (as keyword arguments) |
|:-----------------------|:------------------------|:--------------------------------------------------------------------------|
| `STRING` | `io.String` | `default`, `multiline`, `dynamic_prompts`, `placeholder` |
| `INT` | `io.Int` | `default`, `min`, `max`, `step`, `display_mode`, `control_after_generate` |
| `FLOAT` | `io.Float` | `default`, `min`, `max`, `step`, `round`, `display_mode` |
| `BOOLEAN` | `io.Boolean` | `default`, `label_on`, `label_off` |
| `COMBO` | `io.Combo` | `options`, `default`, `upload`, `image_folder`, `remote` |
| (custom) | `io.MultiCombo` | `options`, `default`, `placeholder`, `chip` |
| `IMAGE` | `io.Image` | |
| `MASK` | `io.Mask` | |
| `MESH` | `io.Mesh` | |
| `HOOKS` | `io.Hooks` | |
| `HOOK_KEYFRAMES` | `io.HookKeyframes` | |
| `LATENT` | `io.Latent` | |
| `LATENT_OPERATION` | `io.LatentOperation` | |
| `LOAD3D_CAMERA` | `io.Load3DCamera` | |
| `LOAD_3D` | `io.Load3D` | |
| `LOAD_3D_ANIMATION` | `io.Load3DAnimation` | |
| `LOSS_MAP` | `io.LossMap` | |
| `LORA_MODEL` | `io.LoraModel` | |
| `CONDITIONING` | `io.Conditioning` | |
| `CLIP` | `io.Clip` | |
| `CLIP_VISION_OUTPUT` | `io.ClipVisionOutput` | |
| `NOISE` | `io.Noise` | |
| `VAE` | `io.Vae` | |
| `MODEL` | `io.Model` | |
| `CONTROL_NET` | `io.ControlNet` | |
| `SAMPLER` | `io.Sampler` | |
| `SIGMAS` | `io.Sigmas` | |
| `GUIDER` | `io.Guider` | |
| `CLIP_VISION` | `io.ClipVision` | |
| `UPSCALE_MODEL` | `io.UpscaleModel` | |
| `AUDIO` | `io.Audio` | |
| `VIDEO` | `io.Video` | |
| `VOXEL` | `io.Voxel` | |
| `WAN_CAMERA_EMBEDDING` | `io.WanCameraEmbedding` | |
| `WEBCAM` | `io.Webcam` | `default`, `socketless` |
| `*` | `io.AnyType` | Used for inputs that can accept any type, like the PreviewAny node. |
#### Advanced Input Types
**MultiType Input (accepts multiple types):**
```python
io.MultiType.Input("input", types=[io.Mask, io.Float, io.Int], optional=True)
```
**Combo with Remote Options:**
```python
io.Combo.Input(
"lora_name",
options=folder_paths.get_filename_list("loras"),
tooltip="The name of the LoRA."
)
```
**Optional Parameters:**
```python
io.Boolean.Input(
"case_sensitive",
default=True,
optional=True, # Makes this input optional
tooltip="Whether to use case-sensitive matching"
)
```
### Step 4: Migrating Logic
- **Execution Method**: Rename your old `FUNCTION` to `execute` and make it a `@classmethod`.
- **Return Value**: Wrap your return tuple in `io.NodeOutput()`. For UI updates, use the `ui` keyword argument: `io.NodeOutput(ui=ui.PreviewImage(image))`.
- **State**: Replace `self.variable` with `cls.state.variable`.
- **Hidden Inputs**: Replace `prompt` and `unique_id` parameters with `cls.hidden.prompt` and `cls.hidden.unique_id`. Request them in the schema with `hidden=[io.Hidden.prompt, io.Hidden.unique_id]`.
- **Optional Methods**: `IS_CHANGED` becomes `fingerprint_inputs`, and `VALIDATE_INPUTS` becomes `validate_inputs`. Both should be `@classmethod`.
## Common Migration Patterns
### 1. Hidden Inputs
**V1:**
```python
"hidden": {
"prompt": "PROMPT",
"unique_id": "UNIQUE_ID"
}
def execute(self, ..., prompt=None, unique_id=None):
... # Use hidden inputs
```
**V3:**
```python
hidden=[
io.Hidden.prompt,
io.Hidden.unique_id
]
@classmethod
def execute(cls, ...):
# Access via **cls**
prompt = cls.hidden.prompt
unique_id = cls.hidden.unique_id
```
### 2. State Management
**V1:**
```python
def __init__(self):
self.last_seed = None
self.cache = {}
def execute(self, seed, ...):
if seed != self.last_seed:
self.cache.clear()
self.last_seed = seed
```
**V3:**
```python
@classmethod
def execute(cls, seed, ...):
if cls.state.last_seed != seed:
cls.state.cache = {}
cls.state.last_seed = seed
```
### 3. UI Output
**V1:**
```python
def execute(self, image):
# Save preview manually
preview = save_temp_image(image)
return {"ui": {"images": preview}, "result": (image,)}
```
**V3:**
```python
@classmethod
def execute(cls, image):
return io.NodeOutput(image, ui=ui.PreviewImage(image))
```
### 4. Dynamic Inputs
**V1:**
```python
@classmethod
def INPUT_TYPES(s):
# Complex logic to generate dynamic inputs
inputs = {"required": {}}
for i in range(get_dynamic_count()):
inputs["required"][f"input_{i}"] = ("IMAGE",)
return inputs
```
**V3:**
```python
inputs=[
io.AutoGrowDynamic.Input("images",
template_input=io.Image.Input("image"),
min=1,
max=10
)
]
```
### 5. Resource Loading
**V1:**
```python
def execute(self, model_name):
# Direct file loading
model_path = folder_paths.get_full_path("checkpoints", model_name)
model = comfy.utils.load_torch_file(model_path)
```
**V3:**
```python
from comfy_api.latest import resources
@classmethod
def execute(cls, model_name):
# Cached resource loading
model = cls.resources.get(
resources.TorchDictFolderFilename("checkpoints", model_name)
)
```
## Making Nodes Async
### Basic Async Node
```python
class AsyncNodeV3(io.ComfyNode):
@classmethod
async def execute(cls, image, url):
# Network request without blocking
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
data = await response.json()
# Process with the data
result = process_image_with_data(image, data)
return io.NodeOutput(result)
```
### Progress Tracking
```python
@classmethod
async def execute(cls, images, unique_id):
from comfy.utils import ProgressBar
batch_size = images.shape[0]
pbar = ProgressBar(batch_size, node_id=unique_id)
results = []
for i in range(batch_size):
# Async processing
result = await process_single(images[i])
results.append(result)
pbar.update(1)
return io.NodeOutput(torch.cat(results))
```
## Enabling Process Isolation
### 1. Create manifest.yaml
```yaml
name: my_custom_nodes
version: 1.0.0
description: My custom node collection
author: Your Name
dependencies:
- numpy==1.26.4
- scikit-image>=0.22.0
- opencv-python
isolated: true
share_torch: true
```
### 2. Update __init__.py
```python
from pyisolate import ExtensionBase
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
class MyNodesExtension(ExtensionBase):
def on_module_loaded(self, module):
# Nodes are automatically registered
pass
async def get_node_mappings(self):
return NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
# Extension entry point
def create_extension():
return MyNodesExtension()
```
## Practical Migration Examples
### Complete String Node Conversion
This example shows a full conversion of the StringConcatenate node from v1 to v3:
**V1 Implementation:**
```python
class StringConcatenate():
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"string_a": (IO.STRING, {"multiline": True}),
"string_b": (IO.STRING, {"multiline": True}),
"delimiter": (IO.STRING, {"multiline": False, "default": ""})
}
}
RETURN_TYPES = (IO.STRING,)
FUNCTION = "execute"
CATEGORY = "utils/string"
def execute(self, string_a, string_b, delimiter, **kwargs):
return delimiter.join((string_a, string_b)),
```
**V3 Implementation:**
```python
from comfy_api.latest import io, ui
class StringConcatenate(io.ComfyNode):
"""Concatenates two strings with an optional delimiter between them."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="StringConcatenate",
display_name="String Concatenate",
category="utils/string",
description="Concatenates two strings together with an optional delimiter between them.",
inputs=[
io.String.Input(
"string_a",
display_name="String A",
multiline=True,
tooltip="The first string to concatenate"
),
io.String.Input(
"string_b",
display_name="String B",
multiline=True,
tooltip="The second string to concatenate"
),
io.String.Input(
"delimiter",
display_name="Delimiter",
default="",
multiline=False,
tooltip="The delimiter to insert between the two strings (empty by default)"
),
],
outputs=[
io.String.Output(
"concatenated",
display_name="Concatenated String",
tooltip="The result of concatenating string_a and string_b with the delimiter"
),
],
)
@classmethod
def execute(cls, string_a: str, string_b: str, delimiter: str) -> io.NodeOutput:
"""Concatenates two strings with an optional delimiter."""
result = delimiter.join((string_a, string_b))
return io.NodeOutput(result)
```
### Replacing V1 Nodes Strategy
When replacing v1 nodes with v3 implementations:
1. **Keep Original Node Names**: Don't add "V3" suffix to maintain compatibility
2. **Preserve All Parameters**: Keep same parameter names and defaults
3. **Maintain Return Structure**: v3 automatically generates v1-compatible returns
4. **Test Workflow Compatibility**: Ensure existing workflows continue to work
Example migration workflow:
```bash
# 1. Create new branch
git checkout -b v3-node-migration
# 2. Backup original
cp nodes_original.py nodes_original.py.bak
# 3. Replace with v3 version
cp nodes_v3.py nodes_original.py
# 4. Test with existing workflows
comfy-cli test-workflows ./test-workflows/
```
## Testing Your Migration
### 1. Backward Compatibility Test
```python
# Your v3 node should work with v1 calls
def test_v1_compatibility():
node = MyNodeV3()
inputs = node.INPUT_TYPES()
assert "required" in inputs
assert hasattr(node, "FUNCTION")
assert hasattr(node, "RETURN_TYPES")
```
### 2. Async Execution Test
```python
import asyncio
async def test_async_execution():
result = await MyAsyncNode.execute(image=test_image)
assert result is not None
```
### 3. Isolation Test
```bash
# Test with conflicting dependencies
comfy-cli test-node --isolated my_custom_nodes
```
## Best Practices
1. **Keep nodes stateless** - Use `cls.state` for any mutable data
2. **Make I/O operations async** - Network, disk, database operations
3. **Use resource caching** - Via `cls.resources.get()`
4. **Declare all dependencies** - In manifest.yaml
5. **Test both sync and async** - Ensure compatibility
6. **Document type changes** - Help users update workflows
## Common Issues
### Issue: State not persisting
**Solution:** Use `cls.state` instead of instance variables
### Issue: Hidden inputs not working
**Solution:** Access via `cls.hidden.unique_id` not function parameters
### Issue: Async not executing
**Solution:** Ensure method is `async def` and use `await` for async calls
### Issue: Import errors in isolation
**Solution:** Add all dependencies to manifest.yaml
### Issue: Tensors not sharing
**Solution:** Enable `share_torch: true` in manifest.yaml
@@ -0,0 +1,635 @@
---
applyTo: "**/*.py"
description: "ComfyUI v3 API Reference"
---
# ComfyUI v3 API Reference
Complete reference for the ComfyUI v3 node API, including all types, methods, and decorators.
## Core Classes
### ComfyNodeV3
Base class for all v3 nodes.
```python
from comfy_api.latest import io
class CustomNode(io.ComfyNode):
# Class properties set during execution
state: NodeState # Persistent state storage
resources: Resources # Resource loader with caching
hidden: HiddenHolder # Access to hidden inputs
@classmethod
@abstractmethod
def define_schema(cls) -> io.ComfyNode:
"""Define node schema. Must be overridden."""
pass
@classmethod
@abstractmethod
def execute(cls, **kwargs) -> io.NodeOutput:
"""Execute node logic. Can be async."""
pass
@classmethod
def validate_inputs(cls, **kwargs) -> bool:
"""Optional: Validate inputs before execution."""
pass
@classmethod
def fingerprint_inputs(cls, **kwargs) -> Any:
"""Optional: Generate a fingerprint for caching."""
pass
@classmethod
def GET_SERIALIZERS(cls) -> list[Serializer]:
"""Optional: Define custom serializers."""
return []
```
### io.ComfyNode
Node definition schema.
```python
@dataclass
class io.ComfyNode:
node_id: str # Globally unique ID
display_name: str = None # UI display name
category: str = "sd" # Node category
inputs: list[InputV3] = None # Input definitions
outputs: list[OutputV3] = None # Output definitions
hidden: list[Hidden] = None # Hidden inputs
description: str = "" # Tooltip description
is_input_list: bool = False # Handle list inputs
is_output_node: bool = False # Force execution
is_deprecated: bool = False # Mark as deprecated
is_experimental: bool = False # Mark as experimental
is_api_node: bool = False # API node flag
not_idempotent: bool = False # Disable caching
```
### NodeOutput
Structured return value from `execute`.
```python
class NodeOutput:
def __init__(
self,
*args: Any, # Output values
ui: UIOutput | dict = None, # UI elements
expand: dict = None, # Subgraph expansion
block_execution: str = None # Execution blocker
):
pass
```
## Input Types
### Basic Inputs
```python
# Integer input
io.Int.Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: int = None,
min: int = None,
max: int = None,
step: int = None,
control_after_generate: bool = None,
display_mode: NumberDisplay = None,
socketless: bool = None,
force_input: bool = None
)
# Float input
io.Float.Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: float = None,
min: float = None,
max: float = None,
step: float = None,
round: float = None,
display_mode: NumberDisplay = None,
socketless: bool = None,
force_input: bool = None
)
# String input
io.String.Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
multiline: bool = False,
placeholder: str = None,
default: str = None,
dynamic_prompts: bool = None,
socketless: bool = None,
force_input: bool = None
)
# Boolean input
io.Boolean.Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: bool = None,
label_on: str = None,
label_off: str = None,
socketless: bool = None,
force_input: bool = None
)
# Combo (dropdown) input
io.Combo.Input(
id: str,
options: list[str] = None,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: str = None,
control_after_generate: bool = None,
upload: UploadType = None,
image_folder: FolderType = None,
remote: RemoteOptions = None,
socketless: bool = None
)
# Multi-select combo
io.MultiCombo.Input(
id: str,
options: list[str],
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
default: list[str] = None,
placeholder: str = None,
chip: bool = None,
control_after_generate: bool = None,
socketless: bool = None
)
# cusotm type
io.Custom(io_type="MY_TYPE").Input(
id: str,
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None,
placeholder: str = None,
)
```
### ComfyUI Types
```python
# Core types
io.Image.Input(id, ...) # Type: torch.Tensor [B,H,W,C]
io.Mask.Input(id, ...) # Type: torch.Tensor [H,W] or [B,H,W]
io.Latent.Input(id, ...) # Type: dict with 'samples' tensor
io.Conditioning.Input(id, ...) # Type: list[tuple[tensor, dict]]
io.Model.Input(id, ...) # Type: ModelPatcher
io.Clip.Input(id, ...) # Type: CLIP
io.Vae.Input(id, ...) # Type: VAE
io.ControlNet.Input(id, ...) # Type: ControlNet
# Sampling types
io.Sampler.Input(id, ...) # Type: Sampler
io.Sigmas.Input(id, ...) # Type: torch.Tensor
io.Noise.Input(id, ...) # Type: torch.Tensor
io.Guider.Input(id, ...) # Type: CFGGuider
# Additional types
io.ClipVision.Input(id, ...) # Type: ClipVisionModel
io.ClipVisionOutput.Input(id, ...) # Type: ClipVisionOutput
io.StyleModel.Input(id, ...) # Type: StyleModel
io.Gligen.Input(id, ...) # Type: ModelPatcher
io.UpscaleModel.Input(id, ...) # Type: ImageModelDescriptor
io.Audio.Input(id, ...) # Type: dict with 'waveform' and 'sample_rate'
io.Video.Input(id, ...) # Type: VideoInput
io.Webcam.Input(id, ...) # Type: str (filepath)
io.WanCameraEmbedding.Input(id, ...) # Type: torch.Tensor
io.LoraModel.Input(id, ...) # Type: dict[str, Tensor]
io.Hooks.Input(id, ...) # Type: HookGroup
io.HookKeyframes.Input(id, ...) # Type: HookKeyframeGroup
io.SVG.Input(id, ...) # Type: SVG (custom class)
io.Voxel.Input(id, ...) # Type: Voxel data (custom class)
io.Mesh.Input(id, ...) # Type: Mesh data (custom class)
```
### Advanced Inputs
```python
# Multi-type input (accepts multiple types)
io.MultiType.Input(
id: str | InputV3, # Can override from existing input
types: list[type[ComfyType]],
display_name: str = None,
optional: bool = False,
tooltip: str = None,
lazy: bool = None
)
# Dynamic growing input
io.AutogrowDynamic.Input(
id: str,
template_input: InputV3, # Template for each new input
min: int = 1, # Minimum inputs
max: int = None # Maximum inputs
)
# Custom type
@io.comfytype(io_type="MY_CUSTOM")
class MyCustom:
Type = MyDataClass
class Input(io.InputV3):
...
class Output(io.OutputV3):
...
```
## Output Types
```python
# Basic output
io.Image.Output(
id: str = None,
display_name: str = None,
tooltip: str = None,
is_output_list: bool = False # Output is list
)
# All ComfyUI types have corresponding outputs
io.Mask.Output(id, ...)
io.Latent.Output(id, ...)
io.Model.Output(id, ...)
io.Clip.Output(id, ...)
io.Vae.Output(id, ...)
io.Conditioning.Output(id, ...)
io.String.Output(id, ...)
io.Int.Output(id, ...)
io.Float.Output(id, ...)
io.Boolean.Output(id, ...)
# ... etc
```
## Hidden Inputs
```python
from comfy_api.latest import Hidden
# Available hidden inputs
Hidden.unique_id # Node's unique ID
Hidden.prompt # Complete prompt
Hidden.extra_pnginfo # PNG metadata dict
Hidden.dynprompt # Dynamic prompt object
Hidden.auth_token_comfy_org # ComfyOrg auth token
Hidden.api_key_comfy_org # ComfyOrg API key
# Usage in schema
hidden=[
Hidden.unique_id,
Hidden.prompt
]
# Access in execute
unique_id = cls.hidden.unique_id
prompt = cls.hidden.prompt
```
## State Management
```python
# NodeState interface
class NodeState:
def get_value(self, key: str) -> Any
def set_value(self, key: str, value: Any)
def pop(self, key: str) -> Any
def __contains__(self, key: str) -> bool
# Attribute access
cls.state.my_value = 42
value = cls.state.my_value
# Dictionary access
cls.state["key"] = "value"
value = cls.state["key"]
```
## Practical Input/Output Examples
### Enhanced Documentation with Tooltips and Display Names
```python
# String input with full documentation
io.String.Input(
"prompt",
display_name="Text Prompt",
multiline=True,
default="A beautiful landscape",
tooltip="Enter the text description for image generation",
placeholder="Type your prompt here..."
)
# Integer with constraints and UI hints
io.Int.Input(
"steps",
display_name="Sampling Steps",
default=20,
min=1,
max=150,
tooltip="Number of denoising steps. Higher values take longer but may produce better results",
display_mode=io.NumberDisplay.slider
)
# Combo with dynamic options
io.Combo.Input(
"checkpoint",
options=folder_paths.get_filename_list("checkpoints"),
display_name="Model Checkpoint",
tooltip="Select the AI model to use for generation"
)
# Output with documentation
io.Image.Output(
"generated_image",
display_name="Generated Image",
tooltip="The final generated image based on your prompt"
)
# Combo with dynamic options and file upload
io.Combo.Input(
"audio_file",
options=sorted(folder_paths.filter_files_content_types(os.listdir(folder_paths.get_input_directory()), ["audio", "video"])),
display_name="Audio File",
tooltip="Select an audio file or upload a new one",
upload=io.UploadType.audio
)
```
### Return Pattern with NodeOutput
```python
@classmethod
def execute(cls, text: str, count: int) -> io.NodeOutput:
# Single output
result = process_text(text, count)
return io.NodeOutput(result)
# Multiple outputs
image, mask = generate_image_and_mask(text)
return io.NodeOutput(image, mask)
# With UI preview
image = generate_image(text)
return io.NodeOutput(image, ui=ui.PreviewImage(image))
# With multiple UI elements
images = batch_generate(text, count)
previews = [ui.PreviewImage(img) for img in images]
return io.NodeOutput(images, ui={"images": previews})
```
## Resource Management
```python
# Load cached resources
from comfy_api.latest import resources
# Load torch file
model = cls.resources.get(
resources.TorchDictFolderFilename(
folder_name="checkpoints", # Folder category
file_name="model.safetensors"
)
)
# With default value
model = cls.resources.get(key, default=None)
# Custom resource types (future)
class MyResourceKey(ResourceKey):
Type = MyResourceType
def __init__(self, ...):
pass
```
## UI Output Classes
```python
from comfy_api.latest import ui
# Image preview
ui.PreviewImage(
image: torch.Tensor,
animated: bool = False
)
# Mask preview
ui.PreviewMask(
mask: torch.Tensor,
animated: bool = False
)
# Audio preview
ui.PreviewAudio(
values: list[SavedResult | dict]
)
# Text output
ui.PreviewText(
value: str
)
# 3D preview
ui.PreviewUI3D(
values: list[SavedResult | dict]
)
```
## Decorators and Helpers
```python
# Create custom ComfyType
@io.comfytype(io_type="CUSTOM_TYPE")
class CustomType:
Type = CustomClass
class Input(io.InputV3):
...
class Output(io.OutputV3):
...
# Custom serializer
class MySerializer(Serializer, io_type="MY_TYPE"):
@classmethod
def serialize(cls, obj: Any) -> str:
return json.dumps(obj)
@classmethod
def deserialize(cls, s: str) -> Any:
return json.loads(s)
```
## Async Support
```python
# Async execute
class AsyncNode(io.ComfyNode):
@classmethod
async def execute(cls, **kwargs):
result = await async_operation()
return io.NodeOutput(result)
# Async validation
@classmethod
async def VALIDATE_INPUTS(cls, **kwargs):
is_valid = await check_validity()
return True if is_valid else "Error message"
# Async lazy check
async def check_lazy_status(cls, **kwargs):
needed = await determine_needed_inputs()
return needed # List of input names
```
## Complete Example
```python
from comfy_api.latest import io, ui, resources, io.ComfyNode, io.ComfyNode
import torch
class AdvancedNodeV3(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.ComfyNode(
node_id="AdvancedNode",
display_name="Advanced Node",
category="examples/advanced",
description="Demonstrates v3 features",
inputs=[
# Basic inputs
io.Image.Input("image", tooltip="Input image"),
io.Model.Input("model", tooltip="Model to use"),
# Configured inputs
io.Float.Input("strength",
default=0.75,
min=0.0,
max=1.0,
step=0.05,
display_mode=io.NumberDisplay.slider
),
# Multi-type
io.MultiType.Input("flexible",
types=[io.Image, io.Mask, io.Latent],
optional=True
),
# Dynamic
io.AutoGrowDynamic.Input("extra_images",
template_input=io.Image.Input("img"),
min=0,
max=5
)
],
outputs=[
io.Image.Output("result", tooltip="Processed image"),
io.Latent.Output("latent", is_output_list=True)
],
hidden=[
io.Hidden.unique_id,
io.Hidden.prompt
],
is_output_node=True,
is_experimental=True
)
@classmethod
async def execute(cls, image, model, strength, flexible=None, **kwargs):
# Access state
if cls.state.last_model != model:
cls.state.cache = {}
cls.state.last_model = model
# Load resources
weights = cls.resources.get(
resources.TorchDictFolderFilename("loras", "style.safetensors"),
default=None
)
# Access hidden
node_id = cls.hidden.unique_id
# Process async
result = await process_with_model(image, model, strength)
# Handle dynamic inputs
extra_images = [v for k, v in kwargs.items() if k.startswith("extra_")]
# Return with UI
return io.NodeOutput(
result,
[latent],
ui=ui.PreviewImage(result)
)
@classmethod
async def fingerprint_inputs(cls, strength, **kwargs):
if strength < 0.1:
return "Strength too low for good results"
return True
```
## Type Reference
### Type Mappings
| v3 Type | Python Type | Shape/Format |
|---------|------------|--------------|
| `io.Image.Type` | `torch.Tensor` | `[B,H,W,C]` float32 0-1 |
| `io.Mask.Type` | `torch.Tensor` | `[H,W]` or `[B,H,W]` float32 |
| `io.Latent.Type` | `dict` | `{"samples": tensor, ...}` |
| `io.Conditioning.Type` | `list` | `[(tensor, dict), ...]` |
| `io.Audio.Type` | `dict` | `{"waveform": tensor, "sample_rate": int}` |
| `io.Int.Type` | `int` | Python integer |
| `io.Float.Type` | `float` | Python float |
| `io.String.Type` | `str` | Python string |
| `io.Boolean.Type` | `bool` | Python boolean |
### Enum Types
```python
# Number display modes
io.NumberDisplay.number # Standard input
io.NumberDisplay.slider # Slider widget
io.NumberDisplay.color # Color picker widget
# Folder types
io.FolderType.input # Input folder
io.FolderType.output # Output folder
io.FolderType.temp # Temp folder
# Upload types
io.UploadType.image
io.UploadType.audio
io.UploadType.video
io.UploadType.model
```
+11 -1
View File
@@ -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`
+11 -1
View File
@@ -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
View File
@@ -1,4 +1,4 @@
__version__ = "1.3.4"
__version__ = "1.3.6"
import yaml
import json
+13
View File
@@ -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": {
+2 -1
View File
@@ -3,7 +3,8 @@
"Hotkeys": "快捷键",
"Nodes": "节点相关",
"NodesMap": "管理节点组",
"StylesSelector": "样式选择器"
"StylesSelector": "样式选择器",
"MultiAngle": "摄影机多角度提示词"
},
"nodeCategories": {
"Util": "工具",
+13
View File
@@ -393,6 +393,19 @@
}
}
},
"easy multiAngle":{
"display_name": "多视角提示词",
"inputs": {
},
"outputs": {
"0": {
"name": "提示词"
},
"1":{
"name": "参数"
}
}
},
"easy fullLoader": {
"display_name": "简易加载器 (完整版)",
"inputs": {
+11
View File
@@ -71,5 +71,16 @@
"Grid": "网格",
"List": "列表"
}
},
"EasyUse_MultiAngle_InvertRotate": {
"name": "启用反转旋转模式",
"tooltip": "在多角度节点中启用反转旋转模式,使旋转方向与大多数3D软件一致"
},
"EasyUse_MultiAngle_HollowMode": {
"name": "启用多角度镂空展示模式",
"tooltip": "在多角度节点中启用镂空展示模式,可以更直观地查看相机角度"
},
"EasyUse_MultiAngle_AddAnglePrompt": {
"name": "启用添加多角度提示词"
}
}
+133
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
-50
View File
@@ -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
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