Files
yolain-ComfyUI-Easy-Use/py/easyNodes.py
T
2024-02-08 00:43:31 +08:00

5435 lines
233 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
import os
import re
import json
import time
import math
import torch
import psutil
import datetime
import comfy.sd
import comfy.utils
import numpy as np
import folder_paths
import comfy.samplers
import comfy.controlnet
import latent_preview
import comfy.model_base
import comfy.model_management
from comfy.sd import CLIP, VAE
from pathlib import Path
from urllib.request import urlopen
from collections import defaultdict
from PIL.PngImagePlugin import PngInfo
from PIL import Image, ImageDraw, ImageFont
from comfy.model_patcher import ModelPatcher
from comfy_extras.chainner_models import model_loading
from typing import Dict, List, Optional, Tuple, Union, Any
from .adv_encode import advanced_encode, advanced_encode_XL
from server import PromptServer
from nodes import VAELoader, MAX_RESOLUTION, RepeatLatentBatch, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, ConditioningSetMask, ConditioningConcat
from comfy_extras.nodes_mask import LatentCompositeMasked
from .config import BASE_RESOLUTIONS, RESOURCES_DIR, INPAINT_DIR, FOOOCUS_STYLES_DIR, FOOOCUS_INPAINT_HEAD, FOOOCUS_INPAINT_PATCH
from .log import log_node_info, log_node_error, log_node_warn, log_node_success
from .wildcards import process_with_loras, get_wildcard_list, process
# 加载器
class easyLoader:
def __init__(self):
self.loaded_objects = {
"ckpt": defaultdict(tuple), # {ckpt_name: (model, ...)}
"clip": defaultdict(tuple),
"clip_vision": defaultdict(tuple),
"bvae": defaultdict(tuple),
"vae": defaultdict(object),
"lora": defaultdict(dict), # {lora_name: {UID: (model_lora, clip_lora)}}
}
self.memory_threshold = self.determine_memory_threshold(0.7)
def clean_values(self, values: str):
original_values = values.split("; ")
cleaned_values = []
for value in original_values:
cleaned_value = value.strip(';').strip()
if cleaned_value == "":
continue
try:
cleaned_value = int(cleaned_value)
except ValueError:
try:
cleaned_value = float(cleaned_value)
except ValueError:
pass
cleaned_values.append(cleaned_value)
return cleaned_values
def clear_unused_objects(self, desired_names: set, object_type: str):
keys = set(self.loaded_objects[object_type].keys())
for key in keys - desired_names:
del self.loaded_objects[object_type][key]
def get_input_value(self, entry, key):
val = entry["inputs"][key]
return val if isinstance(val, str) else val[0]
def process_pipe_loader(self, entry,
desired_ckpt_names, desired_vae_names,
desired_lora_names, desired_lora_settings, num_loras=3, suffix=""):
for idx in range(1, num_loras + 1):
lora_name_key = f"{suffix}lora{idx}_name"
desired_lora_names.add(self.get_input_value(entry, lora_name_key))
setting = f'{self.get_input_value(entry, lora_name_key)};{entry["inputs"][f"{suffix}lora{idx}_model_strength"]};{entry["inputs"][f"{suffix}lora{idx}_clip_strength"]}'
desired_lora_settings.add(setting)
desired_ckpt_names.add(self.get_input_value(entry, f"{suffix}ckpt_name"))
desired_vae_names.add(self.get_input_value(entry, f"{suffix}vae_name"))
def update_loaded_objects(self, prompt):
desired_ckpt_names = set()
desired_vae_names = set()
desired_lora_names = set()
desired_lora_settings = set()
for entry in prompt.values():
class_type = entry["class_type"]
if class_type == "easy a1111Loader" or class_type == "easy comfyLoader":
lora_name = self.get_input_value(entry, "lora_name")
desired_lora_names.add(lora_name)
setting = f'{lora_name};{entry["inputs"]["lora_model_strength"]};{entry["inputs"]["lora_clip_strength"]}'
desired_lora_settings.add(setting)
desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name"))
desired_vae_names.add(self.get_input_value(entry, "vae_name"))
elif class_type == "easy zero123Loader" or class_type == 'easy svdLoader':
desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name"))
desired_vae_names.add(self.get_input_value(entry, "vae_name"))
elif class_type == "easy XYInputs: ModelMergeBlocks":
desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name_1"))
desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name_2"))
vae_use = self.get_input_value(entry, "vae_use")
if vae_use != 'Use Model 1' and vae_use != 'Use Model 2':
desired_vae_names.add(vae_use)
object_types = ["ckpt", "clip", "bvae", "vae", "lora"]
for object_type in object_types:
desired_names = desired_ckpt_names if object_type in ["ckpt", "clip", "bvae"] else desired_vae_names if object_type == "vae" else desired_lora_names
self.clear_unused_objects(desired_names, object_type)
def add_to_cache(self, obj_type, key, value):
"""
Add an item to the cache with the current timestamp.
"""
timestamped_value = (value, time.time())
self.loaded_objects[obj_type][key] = timestamped_value
def determine_memory_threshold(self, percentage=0.8):
"""
Determines the memory threshold as a percentage of the total available memory.
Args:
- percentage (float): The fraction of total memory to use as the threshold.
Should be a value between 0 and 1. Default is 0.8 (80%).
Returns:
- memory_threshold (int): Memory threshold in bytes.
"""
total_memory = psutil.virtual_memory().total
memory_threshold = total_memory * percentage
return memory_threshold
def get_memory_usage(self):
"""
Returns the memory usage of the current process in bytes.
"""
process = psutil.Process(os.getpid())
return process.memory_info().rss
def eviction_based_on_memory(self):
"""
Evicts objects from cache based on memory usage and priority.
"""
current_memory = self.get_memory_usage()
if current_memory < self.memory_threshold:
return
eviction_order = ["vae", "lora", "bvae", "clip", "ckpt"]
for obj_type in eviction_order:
if current_memory < self.memory_threshold:
break
# Sort items based on age (using the timestamp)
items = list(self.loaded_objects[obj_type].items())
items.sort(key=lambda x: x[1][1]) # Sorting by timestamp
for item in items:
if current_memory < self.memory_threshold:
break
del self.loaded_objects[obj_type][item[0]]
current_memory = self.get_memory_usage()
def load_checkpoint(self, ckpt_name, config_name=None, load_vision=False):
cache_name = ckpt_name
if config_name not in [None, "Default"]:
cache_name = ckpt_name + "_" + config_name
if cache_name in self.loaded_objects["ckpt"]:
cache_out = self.loaded_objects["clip_vision"][cache_name][0] if load_vision else self.loaded_objects["clip"][cache_name][0]
return self.loaded_objects["ckpt"][cache_name][0], cache_out, self.loaded_objects["bvae"][cache_name][0]
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
output_clip = False if load_vision else True
output_clipvision = True if load_vision else False
if config_name not in [None, "Default"]:
config_path = folder_paths.get_full_path("configs", config_name)
loaded_ckpt = comfy.sd.load_checkpoint(config_path, ckpt_path, output_vae=True, output_clip=output_clip, output_clipvision=output_clipvision,
embedding_directory=folder_paths.get_folder_paths("embeddings"))
else:
loaded_ckpt = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=output_clip, output_clipvision=output_clipvision, embedding_directory=folder_paths.get_folder_paths("embeddings"))
self.add_to_cache("ckpt", cache_name, loaded_ckpt[0])
self.add_to_cache("bvae", cache_name, loaded_ckpt[2])
if load_vision:
out = loaded_ckpt[3]
self.add_to_cache("clip_vision", cache_name, out)
else:
out = loaded_ckpt[1]
self.add_to_cache("clip", cache_name, loaded_ckpt[1])
self.eviction_based_on_memory()
return loaded_ckpt[0], out, loaded_ckpt[2]
def load_vae(self, vae_name):
if vae_name in self.loaded_objects["vae"]:
return self.loaded_objects["vae"][vae_name][0]
vae_path = folder_paths.get_full_path("vae", vae_name)
sd = comfy.utils.load_torch_file(vae_path)
loaded_vae = comfy.sd.VAE(sd=sd)
self.add_to_cache("vae", vae_name, loaded_vae)
self.eviction_based_on_memory()
return loaded_vae
def load_lora(self, lora_name, model, clip, strength_model, strength_clip):
model_hash = str(model)[44:-1]
clip_hash = str(clip)[25:-1]
unique_id = f'{model_hash};{clip_hash};{lora_name};{strength_model};{strength_clip}'
if unique_id in self.loaded_objects["lora"] and unique_id in self.loaded_objects["lora"][lora_name]:
return self.loaded_objects["lora"][unique_id][0]
lora_path = folder_paths.get_full_path("loras", lora_name)
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip)
self.add_to_cache("lora", unique_id, (model_lora, clip_lora))
self.eviction_based_on_memory()
return model_lora, clip_lora
# 采样器
class easySampler:
def __init__(self):
self.last_helds: dict[str, list] = {
"results": [],
"pipe_line": [],
}
@staticmethod
def tensor2pil(image: torch.Tensor) -> Image.Image:
"""Convert a torch tensor to a PIL image."""
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
@staticmethod
def pil2tensor(image: Image.Image) -> torch.Tensor:
"""Convert a PIL image to a torch tensor."""
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
@staticmethod
def enforce_mul_of_64(d):
d = int(d)
if d <= 7:
d = 8
leftover = d % 8 # 8 is the number of pixels per byte
if leftover != 0: # if the number of pixels is not a multiple of 8
if (leftover < 4): # if the number of pixels is less than 4
d -= leftover # remove the leftover pixels
else: # if the number of pixels is more than 4
d += 8 - leftover # add the leftover pixels
return int(d)
@staticmethod
def safe_split(to_split: str, delimiter: str) -> List[str]:
"""Split the input string and return a list of non-empty parts."""
parts = to_split.split(delimiter)
parts = [part for part in parts if part not in ('', ' ', ' ')]
while len(parts) < 2:
parts.append('None')
return parts
def common_ksampler(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0,
disable_noise=False, start_step=None, last_step=None, force_full_denoise=False,
preview_latent=True, disable_pbar=False):
device = comfy.model_management.get_torch_device()
latent_image = latent["samples"]
if disable_noise:
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
else:
batch_inds = latent["batch_index"] if "batch_index" in latent else None
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
noise_mask = None
if "noise_mask" in latent:
noise_mask = latent["noise_mask"]
preview_format = "JPEG"
if preview_format not in ["JPEG", "PNG"]:
preview_format = "JPEG"
previewer = False
if preview_latent:
previewer = latent_preview.get_previewer(device, model.model.latent_format)
pbar = comfy.utils.ProgressBar(steps)
def callback(step, x0, x, total_steps):
preview_bytes = None
if previewer:
preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0)
pbar.update_absolute(step + 1, total_steps, preview_bytes)
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative,
latent_image,
denoise=denoise, disable_noise=disable_noise, start_step=start_step,
last_step=last_step,
force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback,
disable_pbar=disable_pbar, seed=seed)
out = latent.copy()
out["samples"] = samples
return out
def custom_ksampler(self, model, seed, steps, cfg, _sampler, sigmas, positive, negative, latent,
disable_noise=False, preview_latent=True, disable_pbar=False):
device = comfy.model_management.get_torch_device()
latent_image = latent["samples"]
if disable_noise:
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
else:
batch_inds = latent["batch_index"] if "batch_index" in latent else None
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
noise_mask = None
if "noise_mask" in latent:
noise_mask = latent["noise_mask"]
preview_format = "JPEG"
if preview_format not in ["JPEG", "PNG"]:
preview_format = "JPEG"
previewer = False
if preview_latent:
previewer = latent_preview.get_previewer(device, model.model.latent_format)
pbar = comfy.utils.ProgressBar(steps)
def callback(step, x0, x, total_steps):
preview_bytes = None
if previewer:
preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0)
pbar.update_absolute(step + 1, total_steps, preview_bytes)
samples = comfy.sample.sample_custom(model, noise, cfg, _sampler, sigmas, positive, negative, latent_image,
noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar,
seed=seed)
out = latent.copy()
out["samples"] = samples
return out
def get_value_by_id(self, key: str, my_unique_id: Any) -> Optional[Any]:
"""Retrieve value by its associated ID."""
try:
for value, id_ in self.last_helds[key]:
if id_ == my_unique_id:
return value
except KeyError:
return None
def update_value_by_id(self, key: str, my_unique_id: Any, new_value: Any) -> Union[bool, None]:
"""Update the value associated with a given ID. Return True if updated, False if appended, None if key doesn't exist."""
try:
for i, (value, id_) in enumerate(self.last_helds[key]):
if id_ == my_unique_id:
self.last_helds[key][i] = (new_value, id_)
return True
self.last_helds[key].append((new_value, my_unique_id))
return False
except KeyError:
return False
def upscale(self, samples, upscale_method, scale_by, crop):
s = samples.copy()
width = self.enforce_mul_of_64(round(samples["samples"].shape[3] * scale_by))
height = self.enforce_mul_of_64(round(samples["samples"].shape[2] * scale_by))
if (width > MAX_RESOLUTION):
width = MAX_RESOLUTION
if (height > MAX_RESOLUTION):
height = MAX_RESOLUTION
s["samples"] = comfy.utils.common_upscale(samples["samples"], width, height, upscale_method, crop)
return (s,)
def handle_upscale(self, samples: dict, upscale_method: str, factor: float, crop: bool) -> dict:
"""Upscale the samples if the upscale_method is not set to 'None'."""
if upscale_method != "None":
samples = self.upscale(samples, upscale_method, factor, crop)[0]
return samples
def init_state(self, my_unique_id: Any, key: str, default: Any) -> Any:
"""Initialize the state by either fetching the stored value or setting a default."""
value = self.get_value_by_id(key, my_unique_id)
if value is not None:
return value
return default
def get_output(self, pipe: dict,) -> Tuple:
"""Return a tuple of various elements fetched from the input pipe dictionary."""
return (
pipe,
pipe.get("images"),
pipe.get("model"),
pipe.get("positive"),
pipe.get("negative"),
pipe.get("samples"),
pipe.get("vae"),
pipe.get("clip"),
pipe.get("seed"),
)
def get_output_sdxl(self, sdxl_pipe: dict) -> Tuple:
"""Return a tuple of various elements fetched from the input sdxl_pipe dictionary."""
return (
sdxl_pipe,
sdxl_pipe.get("model"),
sdxl_pipe.get("positive"),
sdxl_pipe.get("negative"),
sdxl_pipe.get("vae"),
sdxl_pipe.get("refiner_model"),
sdxl_pipe.get("refiner_positive"),
sdxl_pipe.get("refiner_negative"),
sdxl_pipe.get("refiner_vae"),
sdxl_pipe.get("samples"),
sdxl_pipe.get("clip"),
sdxl_pipe.get("images"),
sdxl_pipe.get("seed")
)
# XY图表
class easyXYPlot:
def __init__(self, xyPlotData, save_prefix, image_output, prompt, extra_pnginfo, my_unique_id):
self.x_node_type, self.x_type = easySampler.safe_split(xyPlotData.get("x_axis"), ': ')
self.y_node_type, self.y_type = easySampler.safe_split(xyPlotData.get("y_axis"), ': ')
self.x_values = xyPlotData.get("x_vals") if self.x_type != "None" else []
self.y_values = xyPlotData.get("y_vals") if self.y_type != "None" else []
self.grid_spacing = xyPlotData.get("grid_spacing")
self.latent_id = 0
self.output_individuals = xyPlotData.get("output_individuals")
self.x_label, self.y_label = [], []
self.max_width, self.max_height = 0, 0
self.latents_plot = []
self.image_list = []
self.num_cols = len(self.x_values) if len(self.x_values) > 0 else 1
self.num_rows = len(self.y_values) if len(self.y_values) > 0 else 1
self.total = self.num_cols * self.num_rows
self.num = 0
self.save_prefix = save_prefix
self.image_output = image_output
self.prompt = prompt
self.extra_pnginfo = extra_pnginfo
self.my_unique_id = my_unique_id
# Helper Functions
@staticmethod
def define_variable(plot_image_vars, value_type, value, index):
plot_image_vars[value_type] = value
if value_type in ["seed", "Seeds++ Batch"]:
value_label = f"{value}"
else:
value_label = f"{value_type}: {value}"
if "ControlNet" in value_type:
value_label = f"ControlNet {index + 1}"
if value_type in ["ModelMergeBlocks"]:
if ":" in value:
line = value.split(':')
value_label = f"{line[0]}"
elif len(value) > 16:
value_label = f"ModelMergeBlocks {index + 1}"
else:
value_label = f"MMB: {value}"
if value_type in ["Pos Condition"]:
value_label = f"pos cond {index + 1}" if index>0 else f"pos cond"
if value_type in ["Neg Condition"]:
value_label = f"neg cond {index + 1}" if index>0 else f"neg cond"
if value_type in ["Positive Prompt S/R"]:
value_label = f"pos prompt {index + 1}" if index>0 else f"pos prompt"
if value_type in ["Negative Prompt S/R"]:
value_label = f"neg prompt {index + 1}" if index>0 else f"neg prompt"
if value_type in ["steps", "cfg", "denoise", "clip_skip",
"lora_model_strength", "lora_clip_strength"]:
value_label = f"{value_type}: {value}"
if value_type == "positive":
value_label = f"pos prompt {index + 1}"
elif value_type == "negative":
value_label = f"neg prompt {index + 1}"
return plot_image_vars, value_label
@staticmethod
def get_font(font_size):
return ImageFont.truetype(str(Path(os.path.join(RESOURCES_DIR, 'OpenSans-Medium.ttf'))), font_size)
@staticmethod
def update_label(label, value, num_items):
if len(label) < num_items:
return [*label, value]
return label
@staticmethod
def rearrange_tensors(latent, num_cols, num_rows):
new_latent = []
for i in range(num_rows):
for j in range(num_cols):
index = j * num_rows + i
new_latent.append(latent[index])
return new_latent
def calculate_background_dimensions(self):
border_size = int((self.max_width // 8) * 1.5) if self.y_type != "None" or self.x_type != "None" else 0
bg_width = self.num_cols * (self.max_width + self.grid_spacing) - self.grid_spacing + border_size * (
self.y_type != "None")
bg_height = self.num_rows * (self.max_height + self.grid_spacing) - self.grid_spacing + border_size * (
self.x_type != "None")
x_offset_initial = border_size if self.y_type != "None" else 0
y_offset = border_size if self.x_type != "None" else 0
return bg_width, bg_height, x_offset_initial, y_offset
def adjust_font_size(self, text, initial_font_size, label_width):
font = self.get_font(initial_font_size)
text_width = font.getbbox(text)
if text_width and text_width[2]:
text_width = text_width[2]
scaling_factor = 0.9
if text_width > (label_width * scaling_factor):
return int(initial_font_size * (label_width / text_width) * scaling_factor)
else:
return initial_font_size
def textsize(self, d, text, font):
_, _, width, height = d.textbbox((0, 0), text=text, font=font)
return width, height
def create_label(self, img, text, initial_font_size, is_x_label=True, max_font_size=70, min_font_size=10):
label_width = img.width if is_x_label else img.height
# Adjust font size
font_size = self.adjust_font_size(text, initial_font_size, label_width)
font_size = min(max_font_size, font_size) # Ensure font isn't too large
font_size = max(min_font_size, font_size) # Ensure font isn't too small
label_height = int(font_size * 1.5) if is_x_label else font_size
label_bg = Image.new('RGBA', (label_width, label_height), color=(255, 255, 255, 0))
d = ImageDraw.Draw(label_bg)
font = self.get_font(font_size)
# Check if text will fit, if not insert ellipsis and reduce text
if self.textsize(d, text, font=font)[0] > label_width:
while self.textsize(d, text + '...', font=font)[0] > label_width and len(text) > 0:
text = text[:-1]
text = text + '...'
# Compute text width and height for multi-line text
text_lines = text.split('\n')
text_widths, text_heights = zip(*[self.textsize(d, line, font=font) for line in text_lines])
max_text_width = max(text_widths)
total_text_height = sum(text_heights)
# Compute position for each line of text
lines_positions = []
current_y = 0
for line, line_width, line_height in zip(text_lines, text_widths, text_heights):
text_x = (label_width - line_width) // 2
text_y = current_y + (label_height - total_text_height) // 2
current_y += line_height
lines_positions.append((line, (text_x, text_y)))
# Draw each line of text
for line, (text_x, text_y) in lines_positions:
d.text((text_x, text_y), line, fill='black', font=font)
return label_bg
def sample_plot_image(self, plot_image_vars, samples, preview_latent, latents_plot, image_list, disable_noise,
start_step, last_step, force_full_denoise, x_value=None, y_value=None):
model, clip, vae, positive, negative, seed, steps, cfg = None, None, None, None, None, None, None, None
sampler_name, scheduler, denoise = None, None, None
# 高级用法
if plot_image_vars["x_node_type"] == "advanced" or plot_image_vars["y_node_type"] == "advanced":
if self.x_type == "Seeds++ Batch" or self.y_type == "Seeds++ Batch":
seed = int(x_value) if self.x_type == "Seeds++ Batch" else int(y_value)
if self.x_type == "Steps" or self.y_type == "Steps":
steps = int(x_value) if self.x_type == "Steps" else int(y_value)
if self.x_type == "StartStep" or self.y_type == "StartStep":
start_step = int(x_value) if self.x_type == "StartStep" else int(y_value)
if self.x_type == "EndStep" or self.y_type == "EndStep":
last_step = int(x_value) if self.x_type == "EndStep" else int(y_value)
if self.x_type == "CFG Scale" or self.y_type == "CFG Scale":
cfg = float(x_value) if self.x_type == "CFG Scale" else float(y_value)
if self.x_type == "Sampler" or self.y_type == "Sampler":
sampler_name = x_value if self.x_type == "Sampler" else y_value
if self.x_type == "Scheduler" or self.y_type == "Scheduler":
scheduler = x_value if self.x_type == "Scheduler" else y_value
if self.x_type == "Sampler&Scheduler" or self.y_type == "Sampler&Scheduler":
arr = x_value.split(',') if self.x_type == "Sampler&Scheduler" else y_value.split(',')
if arr[0] and arr[0]!= 'None':
sampler_name = arr[0]
if arr[1] and arr[1]!= 'None':
scheduler = arr[1]
if self.x_type == "Denoise" or self.y_type == "Denoise":
denoise = float(x_value) if self.x_type == "Denoise" else float(y_value)
if self.x_type == "Pos Condition" or self.y_type == "Pos Condition":
positive = plot_image_vars['positive_cond_stack'][int(x_value)] if self.x_type == "Pos Condition" else plot_image_vars['positive_cond_stack'][int(y_value)]
if self.x_type == "Neg Condition" or self.y_type == "Neg Condition":
negative = plot_image_vars['negative_cond_stack'][int(x_value)] if self.x_type == "Neg Condition" else plot_image_vars['negative_cond_stack'][int(y_value)]
# 模型叠加
if self.x_type == "ModelMergeBlocks" or self.y_type == "ModelMergeBlocks":
ckpt_name_1, ckpt_name_2 = plot_image_vars['models']
model1, clip1, vae1 = easyCache.load_checkpoint(ckpt_name_1)
model2, clip2, vae2 = easyCache.load_checkpoint(ckpt_name_2)
xy_values = x_value if self.x_type == "ModelMergeBlocks" else y_value
if ":" in xy_values:
xy_line = xy_values.split(':')
xy_values = xy_line[1]
xy_arrs = xy_values.split(',')
# ModelMergeBlocks
if len(xy_arrs) == 3:
input, middle, out = xy_arrs
kwargs = {
"input": input,
"middle": middle,
"out": out
}
elif len(xy_arrs) == 30:
kwargs = {}
kwargs["time_embed."] = xy_arrs[0]
kwargs["label_emb."] = xy_arrs[1]
for i in range(12):
kwargs["input_blocks.{}.".format(i)] = xy_arrs[2+i]
for i in range(3):
kwargs["middle_block.{}.".format(i)] = xy_arrs[14+i]
for i in range(12):
kwargs["output_blocks.{}.".format(i)] = xy_arrs[17+i]
kwargs["out."] = xy_arrs[29]
else:
raise Exception("ModelMergeBlocks weight length error")
default_ratio = next(iter(kwargs.values()))
m = model1.clone()
kp = model2.get_key_patches("diffusion_model.")
for k in kp:
ratio = float(default_ratio)
k_unet = k[len("diffusion_model."):]
last_arg_size = 0
for arg in kwargs:
if k_unet.startswith(arg) and last_arg_size < len(arg):
ratio = float(kwargs[arg])
last_arg_size = len(arg)
m.add_patches({k: kp[k]}, 1.0 - ratio, ratio)
vae_use = plot_image_vars['vae_use']
clip = clip2 if vae_use == 'Use Model 2' else clip1
if vae_use == 'Use Model 2':
vae = vae2
elif vae_use == 'Use Model 1':
vae = vae1
else:
(vae,) = VAELoader().load_vae(vae_use)
model = m
# 如果存在lora_stack叠加lora
optional_lora_stack = plot_image_vars['lora_stack']
if optional_lora_stack is not None and optional_lora_stack != []:
for lora in optional_lora_stack:
lora_name = lora["lora_name"]
model = model if model is not None else lora["model"]
clip = clip if clip is not None else lora["clip"]
lora_model_strength = lora["lora_model_strength"]
lora_clip_strength = lora["lora_clip_strength"]
if "lbw" in lora:
lbw = lora["lbw"]
lbw_a = lora["lbw_a"]
lbw_b = lora["lbw_b"]
cls = ALL_NODE_CLASS_MAPPINGS['LoraLoaderBlockWeight //Inspire']
model, clip, _ = cls().doit(model, clip, lora_name, lora_model_strength, lora_clip_strength, False, 0,
lbw_a, lbw_b, "", lbw)
model, clip = easyCache.load_lora(lora_name, model, clip, lora_model_strength, lora_clip_strength)
# 处理clip
clip = clip.clone()
if plot_image_vars['clip_skip'] != 0:
clip.clip_layer(plot_image_vars['clip_skip'])
# 提示词
if "Positive" in self.x_type or "Positive" in self.y_type:
if self.x_type == 'Positive Prompt S/R' or self.y_type == 'Positive Prompt S/R':
positive = x_value if self.x_type == "Positive Prompt S/R" else y_value
if plot_image_vars['a1111_prompt_style']:
if "smZ CLIPTextEncode" in ALL_NODE_CLASS_MAPPINGS:
cls = ALL_NODE_CLASS_MAPPINGS['smZ CLIPTextEncode']
steps = plot_image_vars['steps']
clip = clip if clip is not None else plot_image_vars["clip"]
positive, = cls().encode(clip, positive, "A1111", True, True, False, False, 6,
1024, 1024, 0, 0, 1024, 1024, '', '', steps)
else:
raise Exception(
f"[ERROR] To use clip text encode same as webui, you need to install 'smzNodes'")
else:
clip = clip if clip is not None else plot_image_vars["clip"]
positive = advanced_encode(clip, positive,
plot_image_vars['positive_token_normalization'],
plot_image_vars[
'positive_weight_interpretation'],
w_max=1.0,
apply_to_pooled="enable")
if "positive_cond" in plot_image_vars:
positive = positive + plot_image_vars["positive_cond"]
if "Negative" in self.x_type or "Negative" in self.y_type:
if self.x_type == 'Negative Prompt S/R' or self.y_type == 'Negative Prompt S/R':
negative = x_value if self.x_type == "Negative Prompt S/R" else y_value
if plot_image_vars['a1111_prompt_style']:
if "smZ CLIPTextEncode" in ALL_NODE_CLASS_MAPPINGS:
cls = ALL_NODE_CLASS_MAPPINGS['smZ CLIPTextEncode']
steps = plot_image_vars['steps']
clip = clip if clip is not None else plot_image_vars["clip"]
negative, = cls().encode(clip, negative, "A1111", True, True, False, False, 6,
1024, 1024, 0, 0, 1024, 1024, '', '', steps)
else:
raise Exception(
f"[ERROR] To use clip text encode same as webui, you need to install 'smzNodes'")
else:
clip = clip if clip is not None else plot_image_vars["clip"]
negative = advanced_encode(clip, negative,
plot_image_vars['negative_token_normalization'],
plot_image_vars[
'negative_weight_interpretation'],
w_max=1.0,
apply_to_pooled="enable")
if "negative_cond" in plot_image_vars:
positive = positive + plot_image_vars["negative_cond"]
# ControlNet
if "ControlNet" in self.x_type or "ControlNet" in self.y_type:
_pipe = {
"model": model if model is not None else plot_image_vars["model"],
"positive": positive if positive is not None else plot_image_vars["positive_cond"],
"negative": negative if negative is not None else plot_image_vars["negative_cond"],
"vae": vae if vae is not None else plot_image_vars['vae'],
"clip": clip if clip is not None else plot_image_vars['clip'],
"samples": None,
"images": None,
"loader_settings": {}
}
cnet = plot_image_vars["cnet"] if "cnet" in plot_image_vars else None
if cnet:
index = x_value if "ControlNet" in self.x_type else y_value
controlnet = cnet[index]
for index, item in enumerate(controlnet):
control_net_name = item[0]
image = item[1]
strength = item[2]
start_percent = item[3]
end_percent = item[4]
_pipe, positive, negative = controlnetAdvanced().controlnetApply(_pipe, image, control_net_name, None, strength, start_percent, end_percent, 1)
del _pipe
# 简单用法
if plot_image_vars["x_node_type"] == "loader" or plot_image_vars["y_node_type"] == "loader":
model, clip, vae = easyCache.load_checkpoint(plot_image_vars['ckpt_name'])
if plot_image_vars['lora_name'] != "None":
model, clip = easyCache.load_lora(plot_image_vars['lora_name'], model, clip,
plot_image_vars['lora_model_strength'],
plot_image_vars['lora_clip_strength'])
# Check for custom VAE
if plot_image_vars['vae_name'] not in ["Baked-VAE", "Baked VAE"]:
vae = easyCache.load_vae(plot_image_vars['vae_name'])
# CLIP skip
if not clip:
raise Exception("No CLIP found")
clip = clip.clone()
clip.clip_layer(plot_image_vars['clip_skip'])
if plot_image_vars['a1111_prompt_style']:
if "smZ CLIPTextEncode" in ALL_NODE_CLASS_MAPPINGS:
cls = ALL_NODE_CLASS_MAPPINGS['smZ CLIPTextEncode']
steps = plot_image_vars['steps']
positive, = cls().encode(clip, plot_image_vars['positive'], "A1111", True, True, False, False, 6, 1024, 1024, 0, 0, 1024, 1024, '', '', steps)
negative, = cls().encode(clip, plot_image_vars['negative'], "A1111", True, True, False, False, 6, 1024, 1024, 0, 0, 1024, 1024, '', '', steps)
else:
raise Exception(f"[ERROR] To use clip text encode same as webui, you need to install 'smzNodes'")
else:
positive = advanced_encode(clip, plot_image_vars['positive'],
plot_image_vars['positive_token_normalization'],
plot_image_vars['positive_weight_interpretation'], w_max=1.0,
apply_to_pooled="enable")
negative = advanced_encode(clip, plot_image_vars['negative'],
plot_image_vars['negative_token_normalization'],
plot_image_vars['negative_weight_interpretation'], w_max=1.0,
apply_to_pooled="enable")
model = model if model is not None else plot_image_vars["model"]
clip = clip if clip is not None else plot_image_vars["clip"]
vae = vae if vae is not None else plot_image_vars["vae"]
positive = positive if positive is not None else plot_image_vars["positive_cond"]
negative = negative if negative is not None else plot_image_vars["negative_cond"]
seed = seed if seed is not None else plot_image_vars["seed"]
steps = steps if steps is not None else plot_image_vars["steps"]
cfg = cfg if cfg is not None else plot_image_vars["cfg"]
sampler_name = sampler_name if sampler_name is not None else plot_image_vars["sampler_name"]
scheduler = scheduler if scheduler is not None else plot_image_vars["scheduler"]
denoise = denoise if denoise is not None else plot_image_vars["denoise"]
# Sample
samples = sampler.common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, samples,
denoise=denoise, disable_noise=disable_noise, preview_latent=preview_latent,
start_step=start_step, last_step=last_step,
force_full_denoise=force_full_denoise)
# Decode images and store
latent = samples["samples"]
# Add the latent tensor to the tensors list
latents_plot.append(latent)
# Decode the image
image = vae.decode(latent).cpu()
if self.output_individuals in [True, "True"]:
easy_save = easySave(self.my_unique_id, self.prompt, self.extra_pnginfo)
easy_save.images(image, self.save_prefix, self.image_output, group_id=self.num)
# Convert the image from tensor to PIL Image and add it to the list
pil_image = easySampler.tensor2pil(image)
image_list.append(pil_image)
# Update max dimensions
self.max_width = max(self.max_width, pil_image.width)
self.max_height = max(self.max_height, pil_image.height)
# Return the touched variables
return image_list, self.max_width, self.max_height, latents_plot
# Process Functions
def validate_xy_plot(self):
if self.x_type == 'None' and self.y_type == 'None':
log_node_warn(f'easyKsampler[{self.my_unique_id}]','No Valid Plot Types - Reverting to default sampling...')
return False
else:
return True
def get_latent(self, samples):
# Extract the 'samples' tensor from the dictionary
latent_image_tensor = samples["samples"]
# Split the tensor into individual image tensors
image_tensors = torch.split(latent_image_tensor, 1, dim=0)
# Create a list of dictionaries containing the individual image tensors
latent_list = [{'samples': image} for image in image_tensors]
# Set latent only to the first latent of batch
if self.latent_id >= len(latent_list):
log_node_warn(f'easy kSampler[{self.my_unique_id}]',f'The selected latent_id ({self.latent_id}) is out of range.')
log_node_warn(f'easy kSampler[{self.my_unique_id}]', f'Automatically setting the latent_id to the last image in the list (index: {len(latent_list) - 1}).')
self.latent_id = len(latent_list) - 1
return latent_list[self.latent_id]
def get_labels_and_sample(self, plot_image_vars, latent_image, preview_latent, start_step, last_step,
force_full_denoise, disable_noise):
for x_index, x_value in enumerate(self.x_values):
plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value,
x_index)
self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values))
if self.y_type != 'None':
for y_index, y_value in enumerate(self.y_values):
plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value,
y_index)
self.y_label = self.update_label(self.y_label, y_value_label, len(self.y_values))
# ttNl(f'{CC.GREY}X: {x_value_label}, Y: {y_value_label}').t(
# f'Plot Values {self.num}/{self.total} ->').p()
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list,
disable_noise, start_step, last_step, force_full_denoise, x_value, y_value)
self.num += 1
else:
# ttNl(f'{CC.GREY}X: {x_value_label}').t(f'Plot Values {self.num}/{self.total} ->').p()
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list, disable_noise,
start_step, last_step, force_full_denoise, x_value)
self.num += 1
# Rearrange latent array to match preview image grid
self.latents_plot = self.rearrange_tensors(self.latents_plot, self.num_cols, self.num_rows)
# Concatenate the tensors along the first dimension (dim=0)
self.latents_plot = torch.cat(self.latents_plot, dim=0)
return self.latents_plot
def plot_images_and_labels(self):
# Calculate the background dimensions
bg_width, bg_height, x_offset_initial, y_offset = self.calculate_background_dimensions()
# Create the white background image
background = Image.new('RGBA', (int(bg_width), int(bg_height)), color=(255, 255, 255, 255))
output_image = []
for row_index in range(self.num_rows):
x_offset = x_offset_initial
for col_index in range(self.num_cols):
index = col_index * self.num_rows + row_index
img = self.image_list[index]
output_image.append(sampler.pil2tensor(img))
background.paste(img, (x_offset, y_offset))
# Handle X label
if row_index == 0 and self.x_type != "None":
label_bg = self.create_label(img, self.x_label[col_index], int(48 * img.width / 512))
label_y = (y_offset - label_bg.height) // 2
background.alpha_composite(label_bg, (x_offset, label_y))
# Handle Y label
if col_index == 0 and self.y_type != "None":
label_bg = self.create_label(img, self.y_label[row_index], int(48 * img.height / 512), False)
label_bg = label_bg.rotate(90, expand=True)
label_x = (x_offset - label_bg.width) // 2
label_y = y_offset + (img.height - label_bg.height) // 2
background.alpha_composite(label_bg, (label_x, label_y))
x_offset += img.width + self.grid_spacing
y_offset += img.height + self.grid_spacing
return (sampler.pil2tensor(background), output_image)
easyCache = easyLoader()
sampler = easySampler()
def check_link_to_clip(node_id, clip_id, visited=None, node=None):
"""Check if a given node links directly or indirectly to a loader node."""
if visited is None:
visited = set()
if node_id in visited:
return False
visited.add(node_id)
if "pipe" in node["inputs"]:
link_ids = node["inputs"]["pipe"]
for id in link_ids:
if id != 0 and id == str(clip_id):
return True
return False
def find_nearest_steps(clip_id, prompt):
"""Find the nearest KSampler or preSampling node that references the given id."""
for id in prompt:
node = prompt[id]
if "Sampler" in node["class_type"] or "sampler" in node["class_type"] or "Sampling" in node["class_type"]:
# Check if this KSampler node directly or indirectly references the given CLIPTextEncode node
if check_link_to_clip(id, clip_id, None, node):
steps = node["inputs"]["steps"] if "steps" in node["inputs"] else 1
return steps
return 1
def find_wildcards_seed(clip_id, text, prompt):
def find_link_clip_id(id, seed, wildcard_id):
node = prompt[id]
if "positive" in node['inputs']:
link_ids = node["inputs"]["positive"]
if type(link_ids) == list:
for id in link_ids:
if id != 0:
if id == wildcard_id:
wildcard_node = prompt[wildcard_id]
seed = wildcard_node["inputs"]["seed_num"] if "seed_num" in wildcard_node["inputs"] else None
return seed
else:
return find_link_clip_id(id, seed, wildcard_id)
else:
return None
else:
return None
if "__" in text:
seed = None
for id in prompt:
node = prompt[id]
if "wildcards" in node["class_type"]:
wildcard_id = id
return find_link_clip_id(str(clip_id), seed, wildcard_id)
return seed
else:
return None
class easySave:
def __init__(self, my_unique_id=0, prompt=None, extra_pnginfo=None, number_padding=5, overwrite_existing=False,
output_dir=folder_paths.get_temp_directory()):
self.number_padding = int(number_padding) if number_padding not in [None, "None", 0] else None
self.overwrite_existing = overwrite_existing
self.my_unique_id = my_unique_id
self.prompt = prompt
self.extra_pnginfo = extra_pnginfo
self.type = 'temp'
self.output_dir = output_dir
if self.output_dir != folder_paths.get_temp_directory():
self.output_dir = self.folder_parser(self.output_dir, self.prompt, self.my_unique_id)
if not os.path.exists(self.output_dir):
self._create_directory(self.output_dir)
@staticmethod
def _create_directory(folder: str):
"""Try to create the directory and log the status."""
log_node_warn(f"Folder {folder} does not exist. Attempting to create...")
if not os.path.exists(folder):
try:
os.makedirs(folder)
log_node_success(f"{folder} Created Successfully")
except OSError:
log_node_error(f"Failed to create folder {folder}")
pass
@staticmethod
def _map_filename(filename: str, filename_prefix: str) -> Tuple[int, str, Optional[int]]:
"""Utility function to map filename to its parts."""
# Get the prefix length and extract the prefix
prefix_len = len(os.path.basename(filename_prefix))
prefix = filename[:prefix_len]
# Search for the primary digits
digits = re.search(r'(\d+)', filename[prefix_len:])
# Search for the number in brackets after the primary digits
group_id = re.search(r'\((\d+)\)', filename[prefix_len:])
return (int(digits.group()) if digits else 0, prefix, int(group_id.group(1)) if group_id else 0)
@staticmethod
def _format_date(text: str, date: datetime.datetime) -> str:
"""Format the date according to specific patterns."""
date_formats = {
'd': lambda d: d.day,
'dd': lambda d: '{:02d}'.format(d.day),
'M': lambda d: d.month,
'MM': lambda d: '{:02d}'.format(d.month),
'h': lambda d: d.hour,
'hh': lambda d: '{:02d}'.format(d.hour),
'm': lambda d: d.minute,
'mm': lambda d: '{:02d}'.format(d.minute),
's': lambda d: d.second,
'ss': lambda d: '{:02d}'.format(d.second),
'y': lambda d: d.year,
'yy': lambda d: str(d.year)[2:],
'yyy': lambda d: str(d.year)[1:],
'yyyy': lambda d: d.year,
}
# We need to sort the keys in reverse order to ensure we match the longest formats first
for format_str in sorted(date_formats.keys(), key=len, reverse=True):
if format_str in text:
text = text.replace(format_str, str(date_formats[format_str](date)))
return text
@staticmethod
def _gather_all_inputs(prompt: Dict[str, dict], unique_id: str, linkInput: str = '',
collected_inputs: Optional[Dict[str, Union[str, List[str]]]] = None) -> Dict[
str, Union[str, List[str]]]:
"""Recursively gather all inputs from the prompt dictionary."""
if prompt == None:
return None
collected_inputs = collected_inputs or {}
prompt_inputs = prompt[str(unique_id)]["inputs"]
for p_input, p_input_value in prompt_inputs.items():
a_input = f"{linkInput}>{p_input}" if linkInput else p_input
if isinstance(p_input_value, list):
easySave._gather_all_inputs(prompt, p_input_value[0], a_input, collected_inputs)
else:
existing_value = collected_inputs.get(a_input)
if existing_value is None:
collected_inputs[a_input] = p_input_value
elif p_input_value not in existing_value:
collected_inputs[a_input] = existing_value + "; " + p_input_value
return collected_inputs
@staticmethod
def _get_filename_with_padding(output_dir, filename, number_padding, group_id, ext):
"""Return filename with proper padding."""
try:
filtered = list(filter(lambda a: a[1] == filename,
map(lambda x: easySave._map_filename(x, filename), os.listdir(output_dir))))
last = max(filtered)[0]
for f in filtered:
if f[0] == last:
if f[2] == 0 or f[2] == group_id:
last += 1
counter = last
except (ValueError, FileNotFoundError):
os.makedirs(output_dir, exist_ok=True)
counter = 1
if group_id == 0:
return f"{filename}.{ext}" if number_padding is None else f"{filename}_{counter:0{number_padding}}.{ext}"
else:
return f"{filename}_({group_id}).{ext}" if number_padding is None else f"{filename}_{counter:0{number_padding}}_({group_id}).{ext}"
@staticmethod
def folder_parser(output_dir: str, prompt: Dict[str, dict], my_unique_id: str):
output_dir = re.sub(r'%date:(.*?)%', lambda m: easySave._format_date(m.group(1), datetime.datetime.now()),
output_dir)
all_inputs = easySave._gather_all_inputs(prompt, my_unique_id)
return re.sub(r'%(.*?)%', lambda m: str(all_inputs.get(m.group(1), '')), output_dir)
def images(self, images, filename_prefix, output_type, embed_workflow=True, ext="png", group_id=0):
FORMAT_MAP = {
"png": "PNG",
"jpg": "JPEG",
"jpeg": "JPEG",
"bmp": "BMP",
"tif": "TIFF",
"tiff": "TIFF"
}
if ext not in FORMAT_MAP:
raise ValueError(f"Unsupported file extension {ext}")
if output_type == "Hide":
return list()
if output_type in ("Save", "Hide/Save"):
output_dir = self.output_dir if self.output_dir != folder_paths.get_temp_directory() else folder_paths.get_output_directory()
self.type = "output"
if output_type == "Preview":
output_dir = self.output_dir
filename_prefix = 'easyPreview'
results = list()
filename_prefix = re.sub(r'%date:(.*?)%', lambda m: easySave._format_date(m.group(1), datetime.datetime.now()),
filename_prefix)
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, output_dir, images[0].shape[1], images[0].shape[0])
for image in images:
img = Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8))
filename = filename.replace("%width%", str(img.size[0])).replace("%height%", str(img.size[1]))
metadata = None
if embed_workflow in (True, "True"):
metadata = PngInfo()
if self.prompt is not None:
metadata.add_text("prompt", json.dumps(self.prompt))
if hasattr(self, 'extra_pnginfo') and self.extra_pnginfo is not None:
for key, value in self.extra_pnginfo.items():
metadata.add_text(key, json.dumps(value))
file = f"{filename}_{counter:05}_.png"
img.save(os.path.join(full_output_folder, file), pnginfo=metadata)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
return results
# ---------------------------------------------------------------提示词 开始----------------------------------------------------------------------#
# 正面提示词
class positivePrompt:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {"required": {
"positive": ("STRING", {"default": "", "multiline": True, "placeholder": "Positive"}),}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("positive",)
FUNCTION = "main"
CATEGORY = "EasyUse/Prompt"
@staticmethod
def main(positive):
return positive,
# 通配符提示词
class wildcardsPrompt:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
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,),
"seed_num": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("text", "populated_text")
OUTPUT_NODE = True
FUNCTION = "main"
CATEGORY = "EasyUse/Prompt"
@staticmethod
def main(*args, **kwargs):
prompt = kwargs["prompt"] if "prompt" in kwargs else None
seed_num = kwargs["seed_num"]
# Clean loaded_objects
if prompt:
easyCache.update_loaded_objects(prompt)
text = kwargs['text']
populated_text = process(text, seed_num)
return {"ui": {"value": [seed_num]}, "result": (text, populated_text)}
# 负面提示词
class negativePrompt:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {"required": {
"negative": ("STRING", {"default": "", "multiline": True, "placeholder": "Negative"}),}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("negative",)
FUNCTION = "main"
CATEGORY = "EasyUse/Prompt"
@staticmethod
def main(negative):
return negative,
# 风格提示词选择器
class stylesPromptSelector:
@classmethod
def INPUT_TYPES(s):
styles = ["fooocus_styles"]
styles_dir = FOOOCUS_STYLES_DIR
for file_name in os.listdir(styles_dir):
file = os.path.join(styles_dir, file_name)
if os.path.isfile(file) and file_name.endswith(".json") and "styles" in file_name.split(".")[0]:
styles.append(file_name.split(".")[0])
return {
"required": {
"styles": (styles, {"default": "fooocus_styles"}),
},
"optional": {
"positive": ("STRING", {"forceInput": True}),
"negative": ("STRING", {"forceInput": True}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("STRING", "STRING",)
RETURN_NAMES = ("positive", "negative",)
CATEGORY = 'EasyUse/Prompt'
FUNCTION = 'run'
OUTPUT_NODE = True
def replace_repeat(self, prompt):
prompt = prompt.replace(",", ",")
arr = prompt.split(",")
if len(arr) != len(set(arr)):
all_weight_prompt = re.findall(re.compile(r'[(](.*?)[)]', re.S), prompt)
if len(all_weight_prompt) > 0:
# others_prompt = prompt
# for w_prompt in all_weight_prompt:
# others_prompt = others_prompt.replace('(','').replace(')','')
# print(others_prompt)
return prompt
else:
for i in range(len(arr)):
arr[i] = arr[i].strip()
arr = list(set(arr))
return ", ".join(arr)
else:
return prompt
def run(self, styles, positive='', negative='', prompt=None, extra_pnginfo=None, my_unique_id=None):
values = []
all_styles = {}
positive_prompt, negative_prompt = '', negative
if styles == "fooocus_styles":
file = os.path.join(RESOURCES_DIR, styles + '.json')
else:
file = os.path.join(RESOURCES_DIR, styles + '.json')
f = open(file, 'r', encoding='utf-8')
data = json.load(f)
f.close()
for d in data:
all_styles[d['name']] = d
if my_unique_id in prompt:
if prompt[my_unique_id]["inputs"]['select_styles']:
values = prompt[my_unique_id]["inputs"]['select_styles'].split(',')
has_prompt = False
for index, val in enumerate(values):
if 'prompt' in all_styles[val]:
if "{prompt}" in all_styles[val]['prompt'] and has_prompt == False:
positive_prompt = all_styles[val]['prompt'].format(prompt=positive)
has_prompt = True
else:
positive_prompt += ', ' + all_styles[val]['prompt'].replace(', {prompt}', '').replace('{prompt}', '')
if 'negative_prompt' in all_styles[val]:
negative_prompt += ', ' + all_styles[val]['negative_prompt'] if negative_prompt else all_styles[val]['negative_prompt']
if has_prompt == False and positive:
positive_prompt = positive + ', '
# 去重
positive_prompt = self.replace_repeat(positive_prompt) if positive_prompt else ''
negative_prompt = self.replace_repeat(negative_prompt) if negative_prompt else ''
return (positive_prompt, negative_prompt)
#promptList
class promptList:
@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",)
}
}
RETURN_TYPES = ("LIST",)
RETURN_NAMES = ("prompt_list",)
FUNCTION = "run"
CATEGORY = "EasyUse/Prompt"
def run(self, **kwargs):
prompts = []
if "optional_prompt_list" in kwargs:
for l in kwargs["optional_prompt_list"]:
prompts.append(l)
# Iterate over the received inputs in sorted order.
for k in sorted(kwargs.keys()):
v = kwargs[k]
# Only process string input ports.
if isinstance(v, str) and v != '':
prompts.append(v)
return (prompts,)
# 肖像大师
# Created by AI Wiz Art (Stefano Flore)
# Version: 2.2
# https://stefanoflore.it
# https://ai-wiz.art
class portraitMaster:
@classmethod
def INPUT_TYPES(s):
max_float_value = 1.95
prompt_path = os.path.join(RESOURCES_DIR, 'portrait_prompt.json')
if not os.path.exists(prompt_path):
response = urlopen('https://raw.githubusercontent.com/yolain/ComfyUI-Easy-Use/main/resources/portrait_prompt.json')
temp_prompt = json.loads(response.read())
prompt_serialized = json.dumps(temp_prompt, indent=4)
with open(prompt_path, "w") as f:
f.write(prompt_serialized)
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": ""}),
}
}
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="-",
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,
wrinkles=0, tanned_skin=0, eyes_details=1, iris_details=1, circular_iris=1, circular_pupil=1,
facial_asymmetry=0, prompt_additional="", prompt_start="", prompt_end="", light_type="-",
light_direction="-", light_weight=0, negative_prompt="", photorealism_improvement="disable", beard="-",
model_pose="-", skin_acne=0):
prompt = []
if gender == "-":
gender = ""
else:
if age <= 25 and gender == 'Woman':
gender = 'girl'
if age <= 25 and gender == 'Man':
gender = 'boy'
gender = " " + gender + " "
if nationality_1 != '-' and nationality_2 != '-':
nationality = f"[{nationality_1}:{nationality_2}:{round(nationality_mix, 2)}]"
elif nationality_1 != '-':
nationality = nationality_1 + " "
elif nationality_2 != '-':
nationality = nationality_2 + " "
else:
nationality = ""
if prompt_start != "":
prompt.append(f"{prompt_start}")
if shot != "-" and shot_weight > 0:
prompt.append(f"({shot}:{round(shot_weight, 2)})")
prompt.append(f"({nationality}{gender}{round(age)}-years-old:1.5)")
if body_type != "-" and body_type_weight > 0:
prompt.append(f"({body_type}, {body_type} body:{round(body_type_weight, 2)})")
if model_pose != "-":
prompt.append(f"({model_pose}:1.5)")
if eyes_color != "-":
prompt.append(f"({eyes_color} eyes:1.25)")
if facial_expression != "-" and facial_expression_weight > 0:
prompt.append(
f"({facial_expression}, {facial_expression} expression:{round(facial_expression_weight, 2)})")
if face_shape != "-" and face_shape_weight > 0:
prompt.append(f"({face_shape} shape face:{round(face_shape_weight, 2)})")
if hair_style != "-":
prompt.append(f"({hair_style} hairstyle:1.25)")
if hair_color != "-":
prompt.append(f"({hair_color} hair:1.25)")
if beard != "-":
prompt.append(f"({beard}:1.15)")
if disheveled != "-" and disheveled > 0:
prompt.append(f"(disheveled:{round(disheveled, 2)})")
if prompt_additional != "":
prompt.append(f"{prompt_additional}")
if skin_details > 0:
prompt.append(f"(skin details, skin texture:{round(skin_details, 2)})")
if skin_pores > 0:
prompt.append(f"(skin pores:{round(skin_pores, 2)})")
if skin_imperfections > 0:
prompt.append(f"(skin imperfections:{round(skin_imperfections, 2)})")
if skin_acne > 0:
prompt.append(f"(acne, skin with acne:{round(skin_acne, 2)})")
if wrinkles > 0:
prompt.append(f"(skin imperfections:{round(wrinkles, 2)})")
if tanned_skin > 0:
prompt.append(f"(tanned skin:{round(tanned_skin, 2)})")
if dimples > 0:
prompt.append(f"(dimples:{round(dimples, 2)})")
if freckles > 0:
prompt.append(f"(freckles:{round(freckles, 2)})")
if moles > 0:
prompt.append(f"(skin pores:{round(moles, 2)})")
if eyes_details > 0:
prompt.append(f"(eyes details:{round(eyes_details, 2)})")
if iris_details > 0:
prompt.append(f"(iris details:{round(iris_details, 2)})")
if circular_iris > 0:
prompt.append(f"(circular iris:{round(circular_iris, 2)})")
if circular_pupil > 0:
prompt.append(f"(circular pupil:{round(circular_pupil, 2)})")
if facial_asymmetry > 0:
prompt.append(f"(facial asymmetry, face asymmetry:{round(facial_asymmetry, 2)})")
if light_type != '-' and light_weight > 0:
if light_direction != '-':
prompt.append(f"({light_type} {light_direction}:{round(light_weight, 2)})")
else:
prompt.append(f"({light_type}:{round(light_weight, 2)})")
if prompt_end != "":
prompt.append(f"{prompt_end}")
prompt = ", ".join(prompt)
prompt = prompt.lower()
if photorealism_improvement == "enable":
prompt = prompt + ", (professional photo, balanced photo, balanced exposure:1.2), (film grain:1.15)"
if photorealism_improvement == "enable":
negative_prompt = negative_prompt + ", (shinny skin, reflections on the skin, skin reflections:1.25)"
log_node_info("Portrait Master as generate the prompt:", prompt)
return (prompt, negative_prompt,)
# 潜空间sigma相乘
class latentNoisy:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"steps": ("INT", {"default": 10000, "min": 0, "max": 10000}),
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"end_at_step": ("INT", {"default": 10000, "min": 1, "max": 10000}),
"source": (["CPU", "GPU"],),
"seed_num": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
"optional": {
"pipe": ("PIPE_LINE",),
"optional_model": ("MODEL",),
"optional_latent": ("LATENT",)
}}
RETURN_TYPES = ("PIPE_LINE", "LATENT", "FLOAT",)
RETURN_NAMES = ("pipe", "latent", "sigma",)
FUNCTION = "run"
CATEGORY = "EasyUse/Latent"
def run(self, sampler_name, scheduler, steps, start_at_step, end_at_step, source, seed_num, pipe=None, optional_model=None, optional_latent=None):
model = optional_model if optional_model is not None else pipe["model"]
batch_size = pipe["loader_settings"]["batch_size"]
empty_latent_height = pipe["loader_settings"]["empty_latent_height"]
empty_latent_width = pipe["loader_settings"]["empty_latent_width"]
if optional_latent is not None:
samples = optional_latent
else:
torch.manual_seed(seed_num)
if source == "CPU":
device = "cpu"
else:
device = comfy.model_management.get_torch_device()
noise = torch.randn((batch_size, 4, empty_latent_height // 8, empty_latent_width // 8), dtype=torch.float32,
device=device).cpu()
samples = {"samples": noise}
device = comfy.model_management.get_torch_device()
end_at_step = min(steps, end_at_step)
start_at_step = min(start_at_step, end_at_step)
real_model = None
comfy.model_management.load_model_gpu(model)
real_model = model.model
sampler = comfy.samplers.KSampler(real_model, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=1.0, model_options=model.model_options)
sigmas = sampler.sigmas
sigma = sigmas[start_at_step] - sigmas[end_at_step]
sigma /= model.model.latent_format.scale_factor
sigma = sigma.cpu().numpy()
samples_out = samples.copy()
s1 = samples["samples"]
samples_out["samples"] = s1 * sigma
if pipe is None:
pipe = {}
new_pipe = {
**pipe,
"samples": samples_out
}
del pipe
return (new_pipe, samples_out, sigma)
# Latent遮罩复合
class latentCompositeMaskedWithCond:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pipe": ("PIPE_LINE",),
"text_combine": ("LIST",),
"source_latent": ("LATENT",),
"source_mask": ("MASK",),
"destination_mask": ("MASK",),
"text_combine_mode": (["add", "replace", "cover"], {"default": "add"}),
"replace_text": ("STRING", {"default": ""})
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
OUTPUT_IS_LIST = (False, False, True)
RETURN_TYPES = ("PIPE_LINE", "LATENT", "CONDITIONING")
RETURN_NAMES = ("pipe", "latent", "conditioning",)
FUNCTION = "run"
OUTPUT_NODE = True
CATEGORY = "EasyUse/Latent"
def run(self, pipe, text_combine, source_latent, source_mask, destination_mask, text_combine_mode, replace_text, prompt=None, extra_pnginfo=None, my_unique_id=None):
clip = pipe["clip"]
destination_latent = pipe["samples"]
conds = []
for text in text_combine:
if text_combine_mode == 'cover':
positive = text
elif text_combine_mode == 'replace' and replace_text != '':
positive = pipe["loader_settings"]["positive"].replace(replace_text, text)
else:
positive = pipe["loader_settings"]["positive"] + ',' + text
positive_token_normalization = pipe["loader_settings"]["positive_token_normalization"]
positive_weight_interpretation = pipe["loader_settings"]["positive_weight_interpretation"]
a1111_prompt_style = pipe["loader_settings"]["a1111_prompt_style"]
positive_cond = pipe["positive"]
log_node_warn("正在处理提示词编码...")
# Use new clip text encode by smzNodes like same as webui, when if you installed the smzNodes
if a1111_prompt_style:
if "smZ CLIPTextEncode" in ALL_NODE_CLASS_MAPPINGS:
cls = ALL_NODE_CLASS_MAPPINGS['smZ CLIPTextEncode']
steps = pipe["loader_settings"]["steps"] if "steps" in pipe["loader_settings"] else 5
positive_embeddings_final, = cls().encode(clip, positive, "A1111", True, True, False, False, 6, 1024,
1024, 0, 0, 1024, 1024, '', '', steps)
else:
raise Exception(f"[ERROR] To use clip text encode same as webui, you need to install 'smzNodes'")
else:
positive_embeddings_final = advanced_encode(clip, positive,
positive_token_normalization,
positive_weight_interpretation, w_max=1.0,
apply_to_pooled='enable')
# source cond
(cond_1,) = ConditioningSetMask().append(positive_cond, source_mask, "default", 1)
(cond_2,) = ConditioningSetMask().append(positive_embeddings_final, destination_mask, "default", 1)
positive_cond = cond_1 + cond_2
conds.append(positive_cond)
# latent composite masked
(samples,) = LatentCompositeMasked().composite(destination_latent, source_latent, 0, 0, False)
new_pipe = {
**pipe,
"samples": samples,
"loader_settings": {
**pipe["loader_settings"],
"positive": positive,
}
}
del pipe
return (new_pipe, samples, conds)
# 随机种
class easySeed:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"seed_num": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("seed_num",)
FUNCTION = "doit"
CATEGORY = "EasyUse/Seed"
OUTPUT_NODE = True
def doit(self, seed_num=0, prompt=None, extra_pnginfo=None, my_unique_id=None):
return seed_num,
# 全局随机种
class globalSeed:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"value": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}),
"mode": ("BOOLEAN", {"default": True, "label_on": "control_before_generate", "label_off": "control_after_generate"}),
"action": (["fixed", "increment", "decrement", "randomize",
"increment for each node", "decrement for each node", "randomize for each node"], ),
"last_seed": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ()
FUNCTION = "doit"
CATEGORY = "EasyUse/Seed"
OUTPUT_NODE = True
def doit(self, **kwargs):
return {}
#---------------------------------------------------------------提示词 结束------------------------------------------------------------------------#
#---------------------------------------------------------------加载器 开始----------------------------------------------------------------------#
# 简易加载器完整
class fullLoader:
@classmethod
def INPUT_TYPES(cls):
resolution_strings = [f"{width} x {height}" for width, height in BASE_RESOLUTIONS]
a1111_prompt_style_default = False
return {"required": {
"ckpt_name": (folder_paths.get_filename_list("checkpoints"),),
"config_name": (["Default", ] + folder_paths.get_filename_list("configs"), {"default": "Default"}),
"vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
"clip_skip": ("INT", {"default": -1, "min": -24, "max": 0, "step": 1}),
"lora_name": (["None"] + folder_paths.get_filename_list("loras"),),
"lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"resolution": (resolution_strings,),
"empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"positive": ("STRING", {"default": "Positive", "multiline": True}),
"positive_token_normalization": (["none", "mean", "length", "length+mean"],),
"positive_weight_interpretation": (["comfy", "A1111", "comfy++", "compel", "fixed attention"],),
"negative": ("STRING", {"default": "Negative", "multiline": True}),
"negative_token_normalization": (["none", "mean", "length", "length+mean"],),
"negative_weight_interpretation": (["comfy", "A1111", "comfy++", "compel", "fixed attention"],),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
},
"optional": {"model_override": ("MODEL",), "clip_override": ("CLIP",), "vae_override": ("VAE",), "optional_lora_stack": ("LORA_STACK",), "a1111_prompt_style": ("BOOLEAN", {"default": a1111_prompt_style_default}),},
"hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"}
}
RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE", "CLIP")
RETURN_NAMES = ("pipe", "model", "vae", "clip")
FUNCTION = "adv_pipeloader"
CATEGORY = "EasyUse/Loaders"
def adv_pipeloader(self, ckpt_name, config_name, vae_name, clip_skip,
lora_name, lora_model_strength, lora_clip_strength,
resolution, empty_latent_width, empty_latent_height,
positive, positive_token_normalization, positive_weight_interpretation,
negative, negative_token_normalization, negative_weight_interpretation,
batch_size, model_override=None, clip_override=None, vae_override=None, optional_lora_stack=None, a1111_prompt_style=False, prompt=None,
my_unique_id=None
):
model: ModelPatcher | None = None
clip: CLIP | None = None
vae: VAE | None = None
can_load_lora = True
pipe_lora_stack = []
# resolution
if resolution != "自定义 x 自定义":
try:
width, height = map(int, resolution.split(' x '))
empty_latent_width = width
empty_latent_height = height
except ValueError:
raise ValueError("Invalid base_resolution format.")
# Create Empty Latent
latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu()
samples = {"samples": latent}
# Clean models from loaded_objects
easyCache.update_loaded_objects(prompt)
log_node_warn("正在处理模型...")
# 判断是否存在 模型叠加xyplot, 若存在优先缓存第一个模型
xyinputs_id = next((x for x in prompt if str(prompt[x]["class_type"]) == "easy XYInputs: ModelMergeBlocks"), None)
if xyinputs_id is not None:
node = prompt[xyinputs_id]
if "ckpt_name_1" in node["inputs"]:
ckpt_name_1 = node["inputs"]["ckpt_name_1"]
model, clip, vae = easyCache.load_checkpoint(ckpt_name_1)
can_load_lora = False
# Load models
elif model_override is not None and clip_override is not None and vae_override is not None:
model = model_override
clip = clip_override
vae = vae_override
elif model_override is not None:
raise Exception(f"[ERROR] clip or vae is missing")
elif vae_override is not None:
raise Exception(f"[ERROR] model or clip is missing")
elif clip_override is not None:
raise Exception(f"[ERROR] model or vae is missing")
else:
model, clip, vae = easyCache.load_checkpoint(ckpt_name, config_name)
if optional_lora_stack is not None:
for lora in optional_lora_stack:
if can_load_lora:
model, clip = easyCache.load_lora(lora[0], model, clip, lora[1], lora[2])
pipe_lora_stack.append({"lora_name": lora[0], "model": model, "clip": clip, "lora_model_strength": lora[1], "lora_clip_strength": lora[2]})
if lora_name != "None":
if can_load_lora:
model, clip = easyCache.load_lora(lora_name, model, clip, lora_model_strength, lora_clip_strength)
pipe_lora_stack.append({"lora_name": lora_name, "model": model, "clip": clip, "lora_model_strength": lora_model_strength,
"lora_clip_strength": lora_clip_strength})
# Check for custom VAE
if vae_name not in ["Baked VAE", "Baked-VAE"]:
vae = easyCache.load_vae(vae_name)
# CLIP skip
if not clip:
raise Exception("No CLIP found")
# 判断是否连接 styles selector
is_positive_linked_styles_selector = False
inputs_positive_values = prompt[my_unique_id]['inputs']['positive'] if "positive" in prompt[my_unique_id]['inputs'] else None
if type(inputs_positive_values) == list and inputs_positive_values != 'undefined' and inputs_positive_values[0]:
is_positive_linked_styles_selector = True if prompt[inputs_positive_values[0]] and prompt[inputs_positive_values[0]]['class_type'] == 'easy stylesSelector' else False
is_negative_linked_styles_selector = False
inputs_negative_values = prompt[my_unique_id]['inputs']['negative'] if "negative" in prompt[my_unique_id]['inputs'] else None
if type(inputs_negative_values) == list and inputs_negative_values != 'undefined' and inputs_negative_values[0]:
is_negative_linked_styles_selector = True if prompt[inputs_negative_values[0]] and prompt[inputs_negative_values[0]]['class_type'] == 'easy stylesSelector' else False
log_node_warn("正在处理提示词...")
positive_seed = find_wildcards_seed(my_unique_id, positive, prompt)
model, clip, positive, positive_decode, show_positive_prompt, pipe_lora_stack = process_with_loras(positive, model, clip, "Positive", positive_seed, can_load_lora, pipe_lora_stack)
positive_wildcard_prompt = positive_decode if show_positive_prompt or is_positive_linked_styles_selector else ""
negative_seed = find_wildcards_seed(my_unique_id, negative, prompt)
model, clip, negative, negative_decode, show_negative_prompt, pipe_lora_stack = process_with_loras(negative, model, clip,
"Negative", negative_seed, can_load_lora, pipe_lora_stack)
negative_wildcard_prompt = negative_decode if show_negative_prompt or is_negative_linked_styles_selector else ""
clipped = clip.clone()
if clip_skip != 0 and can_load_lora:
clipped.clip_layer(clip_skip)
log_node_warn("正在处理提示词编码...")
# Use new clip text encode by smzNodes like same as webui, when if you installed the smzNodes
if a1111_prompt_style:
if "smZ CLIPTextEncode" in ALL_NODE_CLASS_MAPPINGS:
cls = ALL_NODE_CLASS_MAPPINGS['smZ CLIPTextEncode']
steps = find_nearest_steps(my_unique_id, prompt)
positive_embeddings_final, = cls().encode(clipped, positive, "A1111", True, True, False, False, 6, 1024, 1024, 0, 0, 1024, 1024, '', '', steps)
negative_embeddings_final, = cls().encode(clipped, negative, "A1111", True, True, False, False, 6, 1024, 1024, 0, 0, 1024, 1024, '', '', steps)
else:
raise Exception(f"[ERROR] To use clip text encode same as webui, you need to install 'smzNodes'")
else:
positive_embeddings_final = advanced_encode(clipped, positive, positive_token_normalization,
positive_weight_interpretation, w_max=1.0,
apply_to_pooled='enable')
negative_embeddings_final = advanced_encode(clipped, negative, negative_token_normalization,
negative_weight_interpretation, w_max=1.0,
apply_to_pooled='enable')
image = easySampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0)))
log_node_warn("处理结束...")
pipe = {"model": model,
"positive": positive_embeddings_final,
"negative": negative_embeddings_final,
"vae": vae,
"clip": clipped,
"samples": samples,
"images": image,
"seed": 0,
"loader_settings": {"ckpt_name": ckpt_name,
"vae_name": vae_name,
"lora_name": lora_name,
"lora_model_strength": lora_model_strength,
"lora_clip_strength": lora_clip_strength,
"lora_stack": pipe_lora_stack,
"refiner_ckpt_name": None,
"refiner_vae_name": None,
"refiner_lora_name": None,
"refiner_lora_model_strength": None,
"refiner_lora_clip_strength": None,
"clip_skip": clip_skip,
"a1111_prompt_style": a1111_prompt_style,
"positive": positive,
"positive_l": None,
"positive_g": None,
"positive_token_normalization": positive_token_normalization,
"positive_weight_interpretation": positive_weight_interpretation,
"positive_balance": None,
"negative": negative,
"negative_l": None,
"negative_g": None,
"negative_token_normalization": negative_token_normalization,
"negative_weight_interpretation": negative_weight_interpretation,
"negative_balance": None,
"empty_latent_width": empty_latent_width,
"empty_latent_height": empty_latent_height,
"batch_size": batch_size,
"seed": 0,
"empty_samples": samples, }
}
return {"ui": {"positive": positive_wildcard_prompt, "negative": negative_wildcard_prompt}, "result": (pipe, model, vae, clip)}
# A1111简易加载器
class a1111Loader:
@classmethod
def INPUT_TYPES(cls):
resolution_strings = [f"{width} x {height}" for width, height in BASE_RESOLUTIONS]
a1111_prompt_style_default = False
checkpoints = folder_paths.get_filename_list("checkpoints")
loras = ["None"] + folder_paths.get_filename_list("loras")
return {"required": {
"ckpt_name": (checkpoints,),
"vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
"clip_skip": ("INT", {"default": -1, "min": -24, "max": 0, "step": 1}),
"lora_name": (loras,),
"lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"resolution": (resolution_strings, {"default": "512 x 512"}),
"empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"positive": ("STRING", {"default": "Positive", "multiline": True}),
"negative": ("STRING", {"default": "Negative", "multiline": True}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
},
"optional": {"optional_lora_stack": ("LORA_STACK",), "a1111_prompt_style": ("BOOLEAN", {"default": a1111_prompt_style_default})},
"hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"}
}
RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE")
RETURN_NAMES = ("pipe", "model", "vae")
FUNCTION = "adv_pipeloader"
CATEGORY = "EasyUse/Loaders"
def adv_pipeloader(self, ckpt_name, vae_name, clip_skip,
lora_name, lora_model_strength, lora_clip_strength,
resolution, empty_latent_width, empty_latent_height,
positive, negative, batch_size, optional_lora_stack=None, a1111_prompt_style=False, prompt=None,
my_unique_id=None):
return fullLoader.adv_pipeloader(self, ckpt_name, 'Default', vae_name, clip_skip,
lora_name, lora_model_strength, lora_clip_strength,
resolution, empty_latent_width, empty_latent_height,
positive, 'mean', 'A1111',
negative,'mean','A1111',
batch_size, None, None, None, optional_lora_stack, a1111_prompt_style, prompt,
my_unique_id
)
# Comfy简易加载器
class comfyLoader:
@classmethod
def INPUT_TYPES(cls):
resolution_strings = [f"{width} x {height}" for width, height in BASE_RESOLUTIONS]
return {"required": {
"ckpt_name": (folder_paths.get_filename_list("checkpoints"),),
"vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
"clip_skip": ("INT", {"default": -1, "min": -24, "max": 0, "step": 1}),
"lora_name": (["None"] + folder_paths.get_filename_list("loras"),),
"lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"resolution": (resolution_strings, {"default": "512 x 512"}),
"empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"positive": ("STRING", {"default": "Positive", "multiline": True}),
"negative": ("STRING", {"default": "Negative", "multiline": True}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
},
"optional": {"optional_lora_stack": ("LORA_STACK",)},
"hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"}}
RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE")
RETURN_NAMES = ("pipe", "model", "vae")
FUNCTION = "adv_pipeloader"
CATEGORY = "EasyUse/Loaders"
def adv_pipeloader(self, ckpt_name, vae_name, clip_skip,
lora_name, lora_model_strength, lora_clip_strength,
resolution, empty_latent_width, empty_latent_height,
positive, negative, batch_size, optional_lora_stack=None, prompt=None,
my_unique_id=None):
return fullLoader.adv_pipeloader(self, ckpt_name, 'Default', vae_name, clip_skip,
lora_name, lora_model_strength, lora_clip_strength,
resolution, empty_latent_width, empty_latent_height,
positive, 'none', 'comfy',
negative, 'none', 'comfy',
batch_size, None, None, None, optional_lora_stack, False, prompt,
my_unique_id
)
# Zero123简易加载器 (3D)
try:
from comfy_extras.nodes_stable3d import camera_embeddings
except FileNotFoundError:
log_node_error("EasyUse[zero123Loader]", "请更新ComfyUI到最新版本")
class zero123Loader:
@classmethod
def INPUT_TYPES(cls):
def get_file_list(filenames):
return [file for file in filenames if file != "put_models_here.txt" and "zero123" in file]
return {"required": {
"ckpt_name": (get_file_list(folder_paths.get_filename_list("checkpoints")),),
"vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
"init_image": ("IMAGE",),
"empty_latent_width": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
"empty_latent_height": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
"elevation": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0}),
"azimuth": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0}),
},
"hidden": {"prompt": "PROMPT"}, "my_unique_id": "UNIQUE_ID"}
RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE")
RETURN_NAMES = ("pipe", "model", "vae")
FUNCTION = "adv_pipeloader"
CATEGORY = "EasyUse/Loaders"
def adv_pipeloader(self, ckpt_name, vae_name, init_image, empty_latent_width, empty_latent_height, batch_size, elevation, azimuth, prompt=None, my_unique_id=None):
model: ModelPatcher | None = None
vae: VAE | None = None
clip: CLIP | None = None
clip_vision = None
# Clean models from loaded_objects
easyCache.update_loaded_objects(prompt)
model, clip_vision, vae = easyCache.load_checkpoint(ckpt_name, "Default", True)
output = clip_vision.encode_image(init_image)
pooled = output.image_embeds.unsqueeze(0)
pixels = comfy.utils.common_upscale(init_image.movedim(-1, 1), empty_latent_width, empty_latent_height, "bilinear", "center").movedim(1, -1)
encode_pixels = pixels[:, :, :, :3]
t = vae.encode(encode_pixels)
cam_embeds = camera_embeddings(elevation, azimuth)
cond = torch.cat([pooled, cam_embeds.repeat((pooled.shape[0], 1, 1))], dim=-1)
positive = [[cond, {"concat_latent_image": t}]]
negative = [[torch.zeros_like(pooled), {"concat_latent_image": torch.zeros_like(t)}]]
latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8])
samples = {"samples": latent}
image = easySampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0)))
pipe = {"model": model,
"positive": positive,
"negative": negative,
"vae": vae,
"clip": clip,
"samples": samples,
"images": image,
"seed": 0,
"loader_settings": {"ckpt_name": ckpt_name,
"vae_name": vae_name,
"positive": positive,
"positive_l": None,
"positive_g": None,
"positive_balance": None,
"negative": negative,
"negative_l": None,
"negative_g": None,
"negative_balance": None,
"empty_latent_width": empty_latent_width,
"empty_latent_height": empty_latent_height,
"batch_size": batch_size,
"seed": 0,
"empty_samples": samples, }
}
return (pipe, model, vae)
#svd加载器
class svdLoader:
@classmethod
def INPUT_TYPES(cls):
resolution_strings = [f"{width} x {height}" for width, height in BASE_RESOLUTIONS]
def get_file_list(filenames):
return [file for file in filenames if file != "put_models_here.txt" and "svd" in file]
return {"required": {
"ckpt_name": (get_file_list(folder_paths.get_filename_list("checkpoints")),),
"vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
"clip_name": (["None"] + folder_paths.get_filename_list("clip"),),
"init_image": ("IMAGE",),
"resolution": (resolution_strings, {"default": "1024 x 576"}),
"empty_latent_width": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
"empty_latent_height": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
"video_frames": ("INT", {"default": 14, "min": 1, "max": 4096}),
"motion_bucket_id": ("INT", {"default": 127, "min": 1, "max": 1023}),
"fps": ("INT", {"default": 6, "min": 1, "max": 1024}),
"augmentation_level": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.01})
},
"optional": {
"optional_positive": ("STRING", {"default": "", "multiline": True}),
"optional_negative": ("STRING", {"default": "", "multiline": True}),
},
"hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"}
}
RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE")
RETURN_NAMES = ("pipe", "model", "vae")
FUNCTION = "adv_pipeloader"
CATEGORY = "EasyUse/Loaders"
def adv_pipeloader(self, ckpt_name, vae_name, clip_name, init_image, resolution, empty_latent_width, empty_latent_height, video_frames, motion_bucket_id, fps, augmentation_level, optional_positive=None, optional_negative=None, prompt=None, my_unique_id=None):
model: ModelPatcher | None = None
vae: VAE | None = None
clip: CLIP | None = None
clip_vision = None
# resolution
if resolution != "自定义 x 自定义":
try:
width, height = map(int, resolution.split(' x '))
empty_latent_width = width
empty_latent_height = height
except ValueError:
raise ValueError("Invalid base_resolution format.")
# Clean models from loaded_objects
easyCache.update_loaded_objects(prompt)
model, clip_vision, vae = easyCache.load_checkpoint(ckpt_name, "Default", True)
output = clip_vision.encode_image(init_image)
pooled = output.image_embeds.unsqueeze(0)
pixels = comfy.utils.common_upscale(init_image.movedim(-1, 1), empty_latent_width, empty_latent_height, "bilinear", "center").movedim(1,
-1)
encode_pixels = pixels[:, :, :, :3]
if augmentation_level > 0:
encode_pixels += torch.randn_like(pixels) * augmentation_level
t = vae.encode(encode_pixels)
positive = [[pooled,
{"motion_bucket_id": motion_bucket_id, "fps": fps, "augmentation_level": augmentation_level,
"concat_latent_image": t}]]
negative = [[torch.zeros_like(pooled),
{"motion_bucket_id": motion_bucket_id, "fps": fps, "augmentation_level": augmentation_level,
"concat_latent_image": torch.zeros_like(t)}]]
if clip_name != 'None':
clip_path = folder_paths.get_full_path("clip", clip_name)
clip = comfy.sd.load_clip(ckpt_paths=[clip_path],
embedding_directory=folder_paths.get_folder_paths("embeddings"))
if optional_positive is not None:
tokens = clip.tokenize(optional_positive)
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
positive_embeddings_final = [[cond, {"pooled_output": pooled}]]
positive, = ConditioningConcat().concat(positive, positive_embeddings_final)
if optional_negative is not None:
tokens = clip.tokenize(optional_negative)
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
negative_embeddings_final = [[cond, {"pooled_output": pooled}]]
negative, = ConditioningConcat().concat(negative, negative_embeddings_final)
latent = torch.zeros([video_frames, 4, empty_latent_height // 8, empty_latent_width // 8])
samples = {"samples": latent}
image = easySampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0)))
pipe = {"model": model,
"positive": positive,
"negative": negative,
"vae": vae,
"clip": clip,
"samples": samples,
"images": image,
"seed": 0,
"loader_settings": {"ckpt_name": ckpt_name,
"vae_name": vae_name,
"positive": positive,
"positive_l": None,
"positive_g": None,
"positive_balance": None,
"negative": negative,
"negative_l": None,
"negative_g": None,
"negative_balance": None,
"empty_latent_width": empty_latent_width,
"empty_latent_height": empty_latent_height,
"batch_size": 1,
"seed": 0,
"empty_samples": samples, }
}
return (pipe, model, vae)
# lora
class loraStackLoader:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
max_lora_num = 10
inputs = {
"required": {
"toggle": ([True, False],),
"mode": (["simple", "advanced"],),
"num_loras": ("INT", {"default": 1, "min": 0, "max": max_lora_num}),
},
"optional": {
"optional_lora_stack": ("LORA_STACK",),
},
}
for i in range(1, max_lora_num+1):
inputs["optional"][f"lora_{i}_name"] = (
["None"] + folder_paths.get_filename_list("loras"), {"default": "None"})
inputs["optional"][f"lora_{i}_strength"] = (
"FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
inputs["optional"][f"lora_{i}_model_strength"] = (
"FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
inputs["optional"][f"lora_{i}_clip_strength"] = (
"FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
return inputs
RETURN_TYPES = ("LORA_STACK",)
RETURN_NAMES = ("lora_stack",)
FUNCTION = "stack"
CATEGORY = "EasyUse/Loaders"
def stack(self, toggle, mode, num_loras, lora_stack=None, **kwargs):
if (toggle in [False, None, "False"]) or not kwargs:
return (None,)
loras = []
# Import Stack values
if lora_stack is not None:
loras.extend([l for l in lora_stack if l[0] != "None"])
# Import Lora values
for i in range(1, num_loras + 1):
lora_name = kwargs.get(f"lora_{i}_name")
if not lora_name or lora_name == "None":
continue
if mode == "simple":
lora_strength = float(kwargs.get(f"lora_{i}_strength"))
loras.append((lora_name, lora_strength, lora_strength))
elif mode == "advanced":
model_strength = float(kwargs.get(f"lora_{i}_model_strength"))
clip_strength = float(kwargs.get(f"lora_{i}_clip_strength"))
loras.append((lora_name, model_strength, clip_strength))
return (loras,)
class controlnetNameStack:
def get_file_list(filenames):
return [file for file in filenames if file != "put_models_here.txt" and "lllite" not in file]
controlnets = ["None"] + get_file_list(folder_paths.get_filename_list("controlnet"))
@classmethod
def INPUT_TYPES(s):
return {"required": {},
"optional": {
"switch_1": (["Off", "On"],),
"controlnet_1": (s.controlnets,),
"controlnet_strength_1": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"start_percent_1": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent_1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"switch_2": (["Off", "On"],),
"controlnet_2": (s.controlnets,),
"controlnet_strength_2": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"start_percent_2": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent_2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"switch_3": (["Off", "On"],),
"controlnet_3": (s.controlnets,),
"controlnet_strength_3": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"start_percent_3": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent_3": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"image_1": ("IMAGE",),
"image_2": ("IMAGE",),
"image_3": ("IMAGE",),
"controlnet_stack": ("CONTROL_NET_STACK",)
},
}
# controlnet
class controlnetSimple:
@classmethod
def INPUT_TYPES(s):
def get_file_list(filenames):
return [file for file in filenames if file != "put_models_here.txt" and "lllite" not in file]
return {
"required": {
"pipe": ("PIPE_LINE",),
"image": ("IMAGE",),
"control_net_name": (get_file_list(folder_paths.get_filename_list("controlnet")),),
},
"optional": {
"control_net": ("CONTROL_NET",),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"scale_soft_weights": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},),
}
}
RETURN_TYPES = ("PIPE_LINE", "CONDITIONING", "CONDITIONING")
RETURN_NAMES = ("pipe", "positive", "negative")
OUTPUT_NODE = True
FUNCTION = "controlnetApply"
CATEGORY = "EasyUse/Loaders"
def controlnetApply(self, pipe, image, control_net_name, control_net=None, strength=1, scale_soft_weights=1):
if control_net is None:
if scale_soft_weights < 1:
if "ScaledSoftControlNetWeights" in ALL_NODE_CLASS_MAPPINGS:
soft_weight_cls = ALL_NODE_CLASS_MAPPINGS['ScaledSoftControlNetWeights']
(weights, timestep_keyframe) = soft_weight_cls().load_weights(scale_soft_weights, False)
cn_adv_cls = ALL_NODE_CLASS_MAPPINGS['ControlNetLoaderAdvanced']
control_net, = cn_adv_cls().load_controlnet(control_net_name, timestep_keyframe)
else:
raise Exception(f"[ERROR] To use Scale soft weight, you need to install 'COMFYUI-Advanced-ControlNet'")
else:
controlnet_path = folder_paths.get_full_path("controlnet", control_net_name)
control_net = comfy.controlnet.load_controlnet(controlnet_path)
control_hint = image.movedim(-1, 1)
positive = pipe["positive"]
negative = pipe["negative"]
if strength != 0:
if negative is None:
p = []
for t in positive:
n = [t[0], t[1].copy()]
c_net = control_net.copy().set_cond_hint(control_hint, strength)
if 'control' in t[1]:
c_net.set_previous_controlnet(t[1]['control'])
n[1]['control'] = c_net
n[1]['control_apply_to_uncond'] = True
p.append(n)
positive = p
else:
cnets = {}
out = []
for conditioning in [positive, negative]:
c = []
for t in conditioning:
d = t[1].copy()
prev_cnet = d.get('control', None)
if prev_cnet in cnets:
c_net = cnets[prev_cnet]
else:
c_net = control_net.copy().set_cond_hint(control_hint, strength)
c_net.set_previous_controlnet(prev_cnet)
cnets[prev_cnet] = c_net
d['control'] = c_net
d['control_apply_to_uncond'] = False
n = [t[0], d]
c.append(n)
out.append(c)
positive = out[0]
negative = out[1]
new_pipe = {
"model": pipe['model'],
"positive": positive,
"negative": negative,
"vae": pipe['vae'],
"clip": pipe['clip'],
"samples": pipe["samples"],
"images": pipe["images"],
"seed": 0,
"loader_settings": pipe["loader_settings"]
}
return (new_pipe, positive, negative)
# controlnetADV
class controlnetAdvanced:
@classmethod
def INPUT_TYPES(s):
def get_file_list(filenames):
return [file for file in filenames if file != "put_models_here.txt" and "lllite" not in file]
return {
"required": {
"pipe": ("PIPE_LINE",),
"image": ("IMAGE",),
"control_net_name": (get_file_list(folder_paths.get_filename_list("controlnet")),),
},
"optional": {
"control_net": ("CONTROL_NET",),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"scale_soft_weights": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},),
}
}
RETURN_TYPES = ("PIPE_LINE", "CONDITIONING", "CONDITIONING")
RETURN_NAMES = ("pipe", "positive", "negative")
OUTPUT_NODE = True
FUNCTION = "controlnetApply"
CATEGORY = "EasyUse/Loaders"
def controlnetApply(self, pipe, image, control_net_name, control_net=None, strength=1, start_percent=0, end_percent=1, scale_soft_weights=1):
if control_net is None:
if scale_soft_weights < 1:
if "ScaledSoftControlNetWeights" in ALL_NODE_CLASS_MAPPINGS:
soft_weight_cls = ALL_NODE_CLASS_MAPPINGS['ScaledSoftControlNetWeights']
(weights, timestep_keyframe) = soft_weight_cls().load_weights(scale_soft_weights, False)
cn_adv_cls = ALL_NODE_CLASS_MAPPINGS['ControlNetLoaderAdvanced']
control_net, = cn_adv_cls().load_controlnet(control_net_name, timestep_keyframe)
else:
raise Exception(
f"[ERROR] To use Scale soft weight, you need to install 'COMFYUI-Advanced-ControlNet'")
else:
controlnet_path = folder_paths.get_full_path("controlnet", control_net_name)
control_net = comfy.controlnet.load_controlnet(controlnet_path)
control_hint = image.movedim(-1, 1)
positive = pipe["positive"]
negative = pipe["negative"]
if strength != 0:
if negative is None:
p = []
for t in positive:
n = [t[0], t[1].copy()]
c_net = control_net.copy().set_cond_hint(control_hint, strength)
if 'control' in t[1]:
c_net.set_previous_controlnet(t[1]['control'])
n[1]['control'] = c_net
n[1]['control_apply_to_uncond'] = True
p.append(n)
positive = p
else:
cnets = {}
out = []
for conditioning in [positive, negative]:
c = []
for t in conditioning:
d = t[1].copy()
prev_cnet = d.get('control', None)
if prev_cnet in cnets:
c_net = cnets[prev_cnet]
else:
c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent))
c_net.set_previous_controlnet(prev_cnet)
cnets[prev_cnet] = c_net
d['control'] = c_net
d['control_apply_to_uncond'] = False
n = [t[0], d]
c.append(n)
out.append(c)
positive = out[0]
negative = out[1]
new_pipe = {
"model": pipe['model'],
"positive": positive,
"negative": negative,
"vae": pipe['vae'],
"clip": pipe['clip'],
"samples": pipe["samples"],
"images": pipe["images"],
"seed": 0,
"loader_settings": pipe["loader_settings"]
}
del pipe
return (new_pipe, positive, negative)
# FooocusInpaint (Testing)
from .fooocus import InpaintHead, InpaintWorker, get_local_filepath
inpaint_head_model = None
class fooocusInpaintLoader:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"head": (list(FOOOCUS_INPAINT_HEAD.keys()),),
"patch": (list(FOOOCUS_INPAINT_PATCH.keys()),),
}
}
RETURN_TYPES = ("INPAINT_PATCH",)
RETURN_NAMES = ("patch",)
CATEGORY = "EasyUse/__for_testing"
FUNCTION = "apply"
def apply(self, head, patch):
global inpaint_head_model
head_file = get_local_filepath(FOOOCUS_INPAINT_HEAD[head]["model_url"], INPAINT_DIR)
if inpaint_head_model is None:
inpaint_head_model = InpaintHead()
sd = torch.load(head_file, map_location='cpu')
inpaint_head_model.load_state_dict(sd)
patch_file = get_local_filepath(FOOOCUS_INPAINT_PATCH[patch]["model_url"], INPAINT_DIR)
inpaint_lora = comfy.utils.load_torch_file(patch_file, safe_load=True)
return ((inpaint_head_model, inpaint_lora),)
#---------------------------------------------------------------预采样 开始----------------------------------------------------------------------#
# 预采样设置(基础)
class samplerSettings:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"seed_num": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}),
},
"optional": {
"image_to_latent": ("IMAGE",),
"latent": ("LATENT",),
},
"hidden":
{"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("PIPE_LINE", )
RETURN_NAMES = ("pipe",)
OUTPUT_NODE = True
FUNCTION = "settings"
CATEGORY = "EasyUse/PreSampling"
def settings(self, pipe, steps, cfg, sampler_name, scheduler, denoise, seed_num, image_to_latent=None, latent=None, prompt=None, extra_pnginfo=None, my_unique_id=None):
# 图生图转换
vae = pipe["vae"]
batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1
if image_to_latent is not None:
samples = {"samples": vae.encode(image_to_latent)}
samples = RepeatLatentBatch().repeat(samples, batch_size)[0]
images = image_to_latent
elif latent is not None:
samples = RepeatLatentBatch().repeat(latent, batch_size)[0]
images = pipe["images"]
else:
samples = pipe["samples"]
images = pipe["images"]
new_pipe = {
"model": pipe['model'],
"positive": pipe['positive'],
"negative": pipe['negative'],
"vae": pipe['vae'],
"clip": pipe['clip'],
"samples": samples,
"images": images,
"seed": seed_num,
"loader_settings": {
**pipe["loader_settings"],
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": denoise,
"add_noise": "enabled"
}
}
del pipe
return {"ui": {"value": [seed_num]}, "result": (new_pipe,)}
# 预采样设置(高级)
class samplerSettingsAdvanced:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
"add_noise": (["enable", "disable"],),
"seed_num": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}),
},
"optional": {
"image_to_latent": ("IMAGE",),
"latent": ("LATENT",)
},
"hidden":
{"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("PIPE_LINE", )
RETURN_NAMES = ("pipe",)
OUTPUT_NODE = True
FUNCTION = "settings"
CATEGORY = "EasyUse/PreSampling"
def settings(self, pipe, steps, cfg, sampler_name, scheduler, start_at_step, end_at_step, add_noise, seed_num, image_to_latent=None, latent=None, prompt=None, extra_pnginfo=None, my_unique_id=None):
# 图生图转换
vae = pipe["vae"]
batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1
if image_to_latent is not None:
samples = {"samples": vae.encode(image_to_latent)}
samples = RepeatLatentBatch().repeat(samples, batch_size)[0]
images = image_to_latent
elif latent is not None:
samples = RepeatLatentBatch().repeat(latent, batch_size)[0]
images = pipe["images"]
else:
samples = pipe["samples"]
images = pipe["images"]
new_pipe = {
"model": pipe['model'],
"positive": pipe['positive'],
"negative": pipe['negative'],
"vae": pipe['vae'],
"clip": pipe['clip'],
"samples": samples,
"images": images,
"seed": seed_num,
"loader_settings": {
**pipe["loader_settings"],
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"start_step": start_at_step,
"last_step": end_at_step,
"denoise": 1.0,
"add_noise": add_noise
}
}
del pipe
return {"ui": {"value": [seed_num]}, "result": (new_pipe,)}
# 预采样设置(SDTurbo)
from .gradual_latent_hires_fix import sample_dpmpp_2s_ancestral, sample_dpmpp_2m_sde, sample_lcm, sample_euler_ancestral
class sdTurboSettings:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"pipe": ("PIPE_LINE",),
"steps": ("INT", {"default": 1, "min": 1, "max": 10}),
"cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.SAMPLER_NAMES,),
"eta": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "round": False}),
"s_noise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "round": False}),
"upscale_ratio": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 16.0, "step": 0.01, "round": False}),
"start_step": ("INT", {"default": 5, "min": 0, "max": 1000, "step": 1}),
"end_step": ("INT", {"default": 15, "min": 0, "max": 1000, "step": 1}),
"upscale_n_step": ("INT", {"default": 3, "min": 0, "max": 1000, "step": 1}),
"unsharp_kernel_size": ("INT", {"default": 3, "min": 1, "max": 21, "step": 1}),
"unsharp_sigma": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.01, "round": False}),
"unsharp_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.01, "round": False}),
"seed_num": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("PIPE_LINE",)
RETURN_NAMES = ("pipe",)
OUTPUT_NODE = True
FUNCTION = "settings"
CATEGORY = "EasyUse/PreSampling"
def settings(self, pipe, steps, cfg, sampler_name, eta, s_noise, upscale_ratio, start_step, end_step, upscale_n_step, unsharp_kernel_size, unsharp_sigma, unsharp_strength, seed_num, prompt=None, extra_pnginfo=None, my_unique_id=None):
model = pipe['model']
# sigma
timesteps = torch.flip(torch.arange(1, 11) * 100 - 1, (0,))[:steps]
sigmas = model.model.model_sampling.sigma(timesteps)
sigmas = torch.cat([sigmas, sigmas.new_zeros([1])])
#sampler
sample_function = None
extra_options = {
"eta": eta,
"s_noise": s_noise,
"upscale_ratio": upscale_ratio,
"start_step": start_step,
"end_step": end_step,
"upscale_n_step": upscale_n_step,
"unsharp_kernel_size": unsharp_kernel_size,
"unsharp_sigma": unsharp_sigma,
"unsharp_strength": unsharp_strength,
}
if sampler_name == "euler_ancestral":
sample_function = sample_euler_ancestral
elif sampler_name == "dpmpp_2s_ancestral":
sample_function = sample_dpmpp_2s_ancestral
elif sampler_name == "dpmpp_2m_sde":
sample_function = sample_dpmpp_2m_sde
elif sampler_name == "lcm":
sample_function = sample_lcm
if sample_function is not None:
unsharp_kernel_size = unsharp_kernel_size if unsharp_kernel_size % 2 == 1 else unsharp_kernel_size + 1
extra_options["unsharp_kernel_size"] = unsharp_kernel_size
_sampler = comfy.samplers.KSAMPLER(sample_function, extra_options)
else:
_sampler = comfy.samplers.sampler_object(sampler_name)
extra_options = None
new_pipe = {
"model": pipe['model'],
"positive": pipe['positive'],
"negative": pipe['negative'],
"vae": pipe['vae'],
"clip": pipe['clip'],
"samples": pipe["samples"],
"images": pipe["images"],
"seed": seed_num,
"loader_settings": {
**pipe["loader_settings"],
"extra_options": extra_options,
"sampler": _sampler,
"sigmas": sigmas,
"steps": steps,
"cfg": cfg,
"add_noise": "enabled"
}
}
del pipe
return {"ui": {"value": [seed_num]}, "result": (new_pipe,)}
# 预采样设置(动态CFG)
from .dynthres_core import DynThresh
class dynamicCFGSettings:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"cfg_mode": (DynThresh.Modes,),
"cfg_scale_min": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 100.0, "step": 0.5}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"seed_num": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}),
},
"optional":{
"image_to_latent": ("IMAGE",),
"latent": ("LATENT",)
},
"hidden":
{"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("PIPE_LINE",)
RETURN_NAMES = ("pipe",)
OUTPUT_NODE = True
FUNCTION = "settings"
CATEGORY = "EasyUse/PreSampling"
def settings(self, pipe, steps, cfg, cfg_mode, cfg_scale_min,sampler_name, scheduler, denoise, seed_num, image_to_latent=None, latent=None, prompt=None, extra_pnginfo=None, my_unique_id=None):
dynamic_thresh = DynThresh(7.0, 1.0,"CONSTANT", 0, cfg_mode, cfg_scale_min, 0, 0, 999, False,
"MEAN", "AD", 1)
def sampler_dyn_thresh(args):
input = args["input"]
cond = input - args["cond"]
uncond = input - args["uncond"]
cond_scale = args["cond_scale"]
time_step = args["timestep"]
dynamic_thresh.step = 999 - time_step[0]
return input - dynamic_thresh.dynthresh(cond, uncond, cond_scale, None)
model = pipe['model']
m = model.clone()
m.set_model_sampler_cfg_function(sampler_dyn_thresh)
# 图生图转换
vae = pipe["vae"]
batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1
if image_to_latent is not None:
samples = {"samples": vae.encode(image_to_latent)}
samples = RepeatLatentBatch().repeat(samples, batch_size)[0]
images = image_to_latent
elif latent is not None:
samples = RepeatLatentBatch().repeat(latent, batch_size)[0]
images = pipe["images"]
else:
samples = pipe["samples"]
images = pipe["images"]
new_pipe = {
"model": m,
"positive": pipe['positive'],
"negative": pipe['negative'],
"vae": pipe['vae'],
"clip": pipe['clip'],
"samples": samples,
"images": images,
"seed": seed_num,
"loader_settings": {
**pipe["loader_settings"],
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": denoise
},
}
del pipe
return {"ui": {"value": [seed_num]}, "result": (new_pipe,)}
# 动态CFG
class dynamicThresholdingFull:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"mimic_scale": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.5}),
"threshold_percentile": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"mimic_mode": (DynThresh.Modes,),
"mimic_scale_min": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.5}),
"cfg_mode": (DynThresh.Modes,),
"cfg_scale_min": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.5}),
"sched_val": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.01}),
"separate_feature_channels": (["enable", "disable"],),
"scaling_startpoint": (DynThresh.Startpoints,),
"variability_measure": (DynThresh.Variabilities,),
"interpolate_phi": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "patch"
CATEGORY = "EasyUse/PreSampling"
def patch(self, model, mimic_scale, threshold_percentile, mimic_mode, mimic_scale_min, cfg_mode, cfg_scale_min,
sched_val, separate_feature_channels, scaling_startpoint, variability_measure, interpolate_phi):
dynamic_thresh = DynThresh(mimic_scale, threshold_percentile, mimic_mode, mimic_scale_min, cfg_mode,
cfg_scale_min, sched_val, 0, 999, separate_feature_channels == "enable",
scaling_startpoint, variability_measure, interpolate_phi)
def sampler_dyn_thresh(args):
input = args["input"]
cond = input - args["cond"]
uncond = input - args["uncond"]
cond_scale = args["cond_scale"]
time_step = args["timestep"]
dynamic_thresh.step = 999 - time_step[0]
return input - dynamic_thresh.dynthresh(cond, uncond, cond_scale, None)
m = model.clone()
m.set_model_sampler_cfg_function(sampler_dyn_thresh)
return (m,)
#---------------------------------------------------------------采样器 开始----------------------------------------------------------------------#
# 完整采样器
class samplerFull:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"seed_num": ("INT", {"default": 0, "min": 0, "max": 1125899906842624}),
"model": ("MODEL",),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"latent": ("LATENT",),
"vae": ("VAE",),
"clip": ("CLIP",),
"xyPlot": ("XYPLOT",),
},
"hidden":
{"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
"embeddingsList": (folder_paths.get_filename_list("embeddings"),)
}
}
RETURN_TYPES = ("PIPE_LINE", "IMAGE", "MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "INT",)
RETURN_NAMES = ("pipe", "image", "model", "positive", "negative", "latent", "vae", "clip", "seed",)
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "EasyUse/Sampler"
def run(self, pipe, steps, cfg, sampler_name, scheduler, denoise, image_output, link_id, save_prefix, seed_num=None, model=None, positive=None, negative=None, latent=None, vae=None, clip=None, xyPlot=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False, downscale_options=None):
# Clean loaded_objects
easyCache.update_loaded_objects(prompt)
# my_unique_id = int(my_unique_id)
# if my_unique_id:
# workflow = extra_pnginfo["workflow"]
# node = next((x for x in workflow["nodes"] if str(x["id"]) == my_unique_id), None)
# if node and 'seed_num' in prompt[my_unique_id]['inputs']:
# seed_num = prompt[my_unique_id]['inputs']['seed_num']
# length = len(node["widgets_values"])
# node["widgets_values"][length - 2] = seed_num
easy_save = easySave(my_unique_id, prompt, extra_pnginfo)
samp_model = model if model is not None else pipe["model"]
samp_positive = positive if positive is not None else pipe["positive"]
samp_negative = negative if negative is not None else pipe["negative"]
samp_samples = latent if latent is not None else pipe["samples"]
samp_vae = vae if vae is not None else pipe["vae"]
samp_clip = clip if clip is not None else pipe["clip"]
samp_seed = seed_num if seed_num is not None else pipe['seed']
steps = steps if steps is not None else pipe['loader_settings']['steps']
start_step = pipe['loader_settings']['start_step'] if 'start_step' in pipe['loader_settings'] else 0
last_step = pipe['loader_settings']['last_step'] if 'last_step' in pipe['loader_settings'] else 10000
cfg = cfg if cfg is not None else pipe['loader_settings']['cfg']
sampler_name = sampler_name if sampler_name is not None else pipe['loader_settings']['sampler_name']
scheduler = scheduler if scheduler is not None else pipe['loader_settings']['scheduler']
denoise = denoise if denoise is not None else pipe['loader_settings']['denoise']
add_noise = pipe['loader_settings']['add_noise'] if 'add_noise' in pipe['loader_settings'] else 'enabled'
if start_step is not None and last_step is not None:
force_full_denoise = True
disable_noise = False
if add_noise == "disable":
disable_noise = True
def downscale_model_unet(samp_model):
if downscale_options is None:
return samp_model
# 获取Unet参数
elif "PatchModelAddDownscale" in ALL_NODE_CLASS_MAPPINGS:
cls = ALL_NODE_CLASS_MAPPINGS['PatchModelAddDownscale']
# 自动收缩Unet
if downscale_options['downscale_factor'] is None:
unet_config = samp_model.model.model_config.unet_config
if unet_config is not None and "samples" in samp_samples:
height = samp_samples['samples'].shape[2] * 8
width = samp_samples['samples'].shape[3] * 8
context_dim = unet_config.get('context_dim')
longer_side = width if width > height else height
if context_dim is not None and longer_side > context_dim:
width_downscale_factor = float(width / context_dim)
height_downscale_factor = float(height / context_dim)
if width_downscale_factor > 1.75:
log_node_warn("正在收缩模型Unet...")
log_node_warn("收缩系数:" + str(width_downscale_factor))
(samp_model,) = cls().patch(samp_model, downscale_options['block_number'], width_downscale_factor, 0, 0.35, True, "bicubic",
"bicubic")
elif height_downscale_factor > 1.25:
log_node_warn("正在收缩模型Unet...")
log_node_warn("收缩系数:" + str(height_downscale_factor))
(samp_model,) = cls().patch(samp_model, downscale_options['block_number'], height_downscale_factor, 0, 0.35, True, "bicubic",
"bicubic")
else:
cls = ALL_NODE_CLASS_MAPPINGS['PatchModelAddDownscale']
log_node_warn("正在收缩模型Unet...")
log_node_warn("收缩系数:" + str(downscale_options['downscale_factor']))
(samp_model,) = cls().patch(samp_model, downscale_options['block_number'], downscale_options['downscale_factor'], downscale_options['start_percent'], downscale_options['end_percent'], downscale_options['downscale_after_skip'], downscale_options['downscale_method'], downscale_options['upscale_method'])
return samp_model
def process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive,
samp_negative,
steps, start_step, last_step, cfg, sampler_name, scheduler, denoise,
image_output, link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id,
preview_latent, force_full_denoise=force_full_denoise, disable_noise=disable_noise):
# Downscale Model Unet
if samp_model is not None:
samp_model = downscale_model_unet(samp_model)
# 推理初始时间
start_time = int(time.time() * 1000)
# 开始推理
samp_samples = sampler.common_ksampler(samp_model, samp_seed, steps, cfg, sampler_name, scheduler, samp_positive, samp_negative, samp_samples, denoise=denoise, preview_latent=preview_latent, start_step=start_step, last_step=last_step, force_full_denoise=force_full_denoise, disable_noise=disable_noise)
# 推理结束时间
end_time = int(time.time() * 1000)
# 解码图片
latent = samp_samples["samples"]
# 解码图片
if tile_size is not None:
samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, )
else:
samp_images = samp_vae.decode(latent).cpu()
# 推理总耗时(包含解码)
end_decode_time = int(time.time() * 1000)
spent_time = '扩散:' + str((end_time-start_time)/1000)+'秒, 解码:' + str((end_decode_time-end_time)/1000)+'秒'
results = easy_save.images(samp_images, save_prefix, image_output)
sampler.update_value_by_id("results", my_unique_id, results)
# Clean loaded_objects
easyCache.update_loaded_objects(prompt)
new_pipe = {
"model": samp_model,
"positive": samp_positive,
"negative": samp_negative,
"vae": samp_vae,
"clip": samp_clip,
"samples": samp_samples,
"images": samp_images,
"seed": samp_seed,
"loader_settings": {
**pipe["loader_settings"],
"spent_time": spent_time
}
}
sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe)
del pipe
if image_output in ("Hide", "Hide/Save"):
return {"ui": {},
"result": sampler.get_output(new_pipe, )}
if image_output in ("Sender", "Sender/Save"):
PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results})
return {"ui": {"images": results},
"result": sampler.get_output(new_pipe, )}
def process_xyPlot(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative,
steps, cfg, sampler_name, scheduler, denoise,
image_output, link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id, preview_latent, xyPlot):
sampleXYplot = easyXYPlot(xyPlot, save_prefix, image_output, prompt, extra_pnginfo, my_unique_id)
if not sampleXYplot.validate_xy_plot():
return process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive,
samp_negative, steps, 0, 10000, cfg,
sampler_name, scheduler, denoise, image_output, link_id, save_prefix, tile_size, prompt,
extra_pnginfo, my_unique_id, preview_latent)
# Downscale Model Unet
if samp_model is not None:
samp_model = downscale_model_unet(samp_model)
plot_image_vars = {
"x_node_type": sampleXYplot.x_node_type, "y_node_type": sampleXYplot.y_node_type,
"lora_name": pipe["loader_settings"]["lora_name"] if "lora_name" in pipe["loader_settings"] else None,
"lora_model_strength": pipe["loader_settings"]["lora_model_strength"] if "lora_model_strength" in pipe["loader_settings"] else None,
"lora_clip_strength": pipe["loader_settings"]["lora_clip_strength"] if "lora_clip_strength" in pipe["loader_settings"] else None,
"lora_stack": pipe["loader_settings"]["lora_stack"] if "lora_stack" in pipe["loader_settings"] else None,
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": denoise,
"seed": samp_seed,
"model": samp_model, "vae": samp_vae, "clip": samp_clip, "positive_cond": samp_positive,
"negative_cond": samp_negative,
"ckpt_name": pipe['loader_settings']['ckpt_name'] if "ckpt_name" in pipe["loader_settings"] else None,
"vae_name": pipe['loader_settings']['vae_name'] if "vae_name" in pipe["loader_settings"] else None,
"clip_skip": pipe['loader_settings']['clip_skip'] if "clip_skip" in pipe["loader_settings"] else None,
"positive": pipe['loader_settings']['positive'] if "positive" in pipe["loader_settings"] else None,
"positive_token_normalization": pipe['loader_settings']['positive_token_normalization'] if "positive_token_normalization" in pipe["loader_settings"] else None,
"positive_weight_interpretation": pipe['loader_settings']['positive_weight_interpretation'] if "positive_weight_interpretation" in pipe["loader_settings"] else None,
"negative": pipe['loader_settings']['negative'] if "negative" in pipe["loader_settings"] else None,
"negative_token_normalization": pipe['loader_settings']['negative_token_normalization'] if "negative_token_normalization" in pipe["loader_settings"] else None,
"negative_weight_interpretation": pipe['loader_settings']['negative_weight_interpretation'] if "negative_weight_interpretation" in pipe["loader_settings"] else None,
}
if "models" in pipe["loader_settings"]:
plot_image_vars["models"] = pipe["loader_settings"]["models"]
if "vae_use" in pipe["loader_settings"]:
plot_image_vars["vae_use"] = pipe["loader_settings"]["vae_use"]
if "a1111_prompt_style" in pipe["loader_settings"]:
plot_image_vars["a1111_prompt_style"] = pipe["loader_settings"]["a1111_prompt_style"]
if "cnet_stack" in pipe["loader_settings"]:
plot_image_vars["cnet"] = pipe["loader_settings"]["cnet_stack"]
if "positive_cond_stack" in pipe["loader_settings"]:
plot_image_vars["positive_cond_stack"] = pipe["loader_settings"]["positive_cond_stack"]
if "negative_cond_stack" in pipe["loader_settings"]:
plot_image_vars["negative_cond_stack"] = pipe["loader_settings"]["negative_cond_stack"]
latent_image = sampleXYplot.get_latent(pipe["samples"])
latents_plot = sampleXYplot.get_labels_and_sample(plot_image_vars, latent_image, preview_latent, start_step,
last_step, force_full_denoise, disable_noise)
samp_samples = {"samples": latents_plot}
images, image_list = sampleXYplot.plot_images_and_labels()
samp_images = images
results = easy_save.images(images, save_prefix, image_output)
# Generate output_images
output_images = torch.stack([tensor.squeeze() for tensor in image_list])
sampler.update_value_by_id("results", my_unique_id, results)
# Clean loaded_objects
easyCache.update_loaded_objects(prompt)
new_pipe = {
"model": samp_model,
"positive": samp_positive,
"negative": samp_negative,
"vae": samp_vae,
"clip": samp_clip,
"samples": samp_samples,
"images": output_images,
"seed": samp_seed,
"loader_settings": pipe["loader_settings"],
}
sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe)
del pipe
if image_output in ("Hide", "Hide/Save"):
return sampler.get_output(new_pipe)
return {"ui": {"images": results}, "result": (sampler.get_output(new_pipe))}
preview_latent = True
if image_output in ("Hide", "Hide/Save"):
preview_latent = False
xyplot_id = next((x for x in prompt if "XYPlot" in str(prompt[x]["class_type"])), None)
if xyplot_id is None:
xyPlot = None
else:
xyPlot = pipe["loader_settings"]["xyplot"] if "xyplot" in pipe["loader_settings"] else xyPlot
if xyPlot is not None:
return process_xyPlot(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, steps, cfg, sampler_name, scheduler, denoise, image_output, link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id, preview_latent, xyPlot)
else:
return process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, steps, start_step, last_step, cfg, sampler_name, scheduler, denoise, image_output, link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id, preview_latent, force_full_denoise, disable_noise)
# 简易采样器
class samplerSimple:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],{"default": "Preview"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"model": ("MODEL",),
},
"hidden":
{"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
"embeddingsList": (folder_paths.get_filename_list("embeddings"),)
}
}
RETURN_TYPES = ("PIPE_LINE", "IMAGE",)
RETURN_NAMES = ("pipe", "image",)
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "EasyUse/Sampler"
def run(self, pipe, image_output, link_id, save_prefix, model=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False):
return samplerFull.run(self, pipe, None, None,None,None,None, image_output, link_id, save_prefix,
None, model, None, None, None, None, None, None,
tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise)
# 简易采样器 (Tiled)
class samplerSimpleTiled:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"tile_size": ("INT", {"default": 512, "min": 320, "max": 4096, "step": 64}),
"image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],{"default": "Preview"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"})
},
"optional": {
"model": ("MODEL",),
},
"hidden": {
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
"embeddingsList": (folder_paths.get_filename_list("embeddings"),)
}
}
RETURN_TYPES = ("PIPE_LINE", "IMAGE",)
RETURN_NAMES = ("pipe", "image",)
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "EasyUse/Sampler"
def run(self, pipe, tile_size=512, image_output='preview', link_id=0, save_prefix='ComfyUI', model=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False):
return samplerFull.run(self, pipe, None, None,None,None,None, image_output, link_id, save_prefix,
None, model, None, None, None, None, None, None,
tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise)
# 简易采样器(收缩Unet)
class samplerSimpleDownscaleUnet:
def __init__(self):
pass
upscale_methods = ["bicubic", "nearest-exact", "bilinear", "area", "bislerp"]
@classmethod
def INPUT_TYPES(s):
return {"required":
{"pipe": ("PIPE_LINE",),
"downscale_mode": (["None", "Auto", "Custom"],{"default": "Auto"}),
"block_number": ("INT", {"default": 3, "min": 1, "max": 32, "step": 1}),
"downscale_factor": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 9.0, "step": 0.001}),
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.001}),
"downscale_after_skip": ("BOOLEAN", {"default": True}),
"downscale_method": (s.upscale_methods,),
"upscale_method": (s.upscale_methods,),
"image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],{"default": "Preview"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"model": ("MODEL",),
},
"hidden":
{"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
"embeddingsList": (folder_paths.get_filename_list("embeddings"),)
}
}
RETURN_TYPES = ("PIPE_LINE", "IMAGE",)
RETURN_NAMES = ("pipe", "image",)
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "EasyUse/Sampler"
def run(self, pipe, downscale_mode, block_number, downscale_factor, start_percent, end_percent, downscale_after_skip, downscale_method, upscale_method, image_output, link_id, save_prefix, model=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False):
downscale_options = None
if downscale_mode == 'Auto':
downscale_options = {
"block_number": block_number,
"downscale_factor": None,
"start_percent": 0,
"end_percent":0.35,
"downscale_after_skip": True,
"downscale_method": "bicubic",
"upscale_method": "bicubic"
}
elif downscale_mode == 'Custom':
downscale_options = {
"block_number": block_number,
"downscale_factor": downscale_factor,
"start_percent": start_percent,
"end_percent": end_percent,
"downscale_after_skip": downscale_after_skip,
"downscale_method": downscale_method,
"upscale_method": upscale_method
}
return samplerFull.run(self, pipe, None, None,None,None,None, image_output, link_id, save_prefix,
None, model, None, None, None, None, None, None,
tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise, downscale_options)
# 简易采样器 (内补)
class samplerSimpleInpainting:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"grow_mask_by": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}),
"image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],{"default": "Preview"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"model": ("MODEL",),
"mask": ("MASK",),
"patch": ("INPAINT_PATCH",),
},
"hidden":
{"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
"embeddingsList": (folder_paths.get_filename_list("embeddings"),)
}
}
RETURN_TYPES = ("PIPE_LINE", "IMAGE", "VAE")
RETURN_NAMES = ("pipe", "image", "vae")
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "EasyUse/Sampler"
def run(self, pipe, grow_mask_by, image_output, link_id, save_prefix, model=None, mask=None, patch=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False):
model = model if model is not None else pipe['model']
if mask is not None:
pixels = pipe["images"] if pipe and "images" in pipe else None
if pixels is None:
raise Exception("No Images found")
vae = pipe["vae"] if pipe and "vae" in pipe else None
if pixels is None:
raise Exception("No VAE found")
x = (pixels.shape[1] // 8) * 8
y = (pixels.shape[2] // 8) * 8
mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])),
size=(pixels.shape[1], pixels.shape[2]), mode="bilinear")
pixels = pixels.clone()
if pixels.shape[1] != x or pixels.shape[2] != y:
x_offset = (pixels.shape[1] % 8) // 2
y_offset = (pixels.shape[2] % 8) // 2
pixels = pixels[:, x_offset:x + x_offset, y_offset:y + y_offset, :]
mask = mask[:, :, x_offset:x + x_offset, y_offset:y + y_offset]
if grow_mask_by == 0:
mask_erosion = mask
else:
kernel_tensor = torch.ones((1, 1, grow_mask_by, grow_mask_by))
padding = math.ceil((grow_mask_by - 1) / 2)
mask_erosion = torch.clamp(torch.nn.functional.conv2d(mask.round(), kernel_tensor, padding=padding), 0,
1)
m = (1.0 - mask.round()).squeeze(1)
for i in range(3):
pixels[:, :, :, i] -= 0.5
pixels[:, :, :, i] *= m
pixels[:, :, :, i] += 0.5
t = vae.encode(pixels)
latent = {"samples": t, "noise_mask": (mask_erosion[:, :, :x, :y].round())}
# when patch was linked
if patch is not None:
worker = InpaintWorker(node_name="easy kSamplerInpainting")
model, = worker.patch(model, latent, patch)
new_pipe = {
"model": model,
"positive": pipe['positive'],
"negative": pipe['negative'],
"vae": pipe['vae'],
"clip": pipe['clip'],
"samples": latent,
"images": pipe['images'],
"seed": pipe['seed'],
"loader_settings": pipe["loader_settings"],
}
else:
new_pipe = pipe
del pipe
return samplerFull.run(self, new_pipe, None, None,None,None,None, image_output, link_id, save_prefix,
None, model, None, None, None, None, None, None,
tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise)
# SDTurbo采样器
class samplerSDTurbo:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],{"default": "Preview"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"model": ("MODEL",),
},
"hidden":
{"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO",
"my_unique_id": "UNIQUE_ID",
"embeddingsList": (folder_paths.get_filename_list("embeddings"),)
}
}
RETURN_TYPES = ("PIPE_LINE", "IMAGE",)
RETURN_NAMES = ("pipe", "image",)
OUTPUT_NODE = True
FUNCTION = "run"
CATEGORY = "EasyUse/Sampler"
def run(self, pipe, image_output, link_id, save_prefix, model=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None,):
# Clean loaded_objects
easyCache.update_loaded_objects(prompt)
my_unique_id = int(my_unique_id)
easy_save = easySave(my_unique_id, prompt, extra_pnginfo)
samp_model = pipe["model"] if model is None else model
samp_positive = pipe["positive"]
samp_negative = pipe["negative"]
samp_samples = pipe["samples"]
samp_vae = pipe["vae"]
samp_clip = pipe["clip"]
samp_seed = pipe['seed']
samp_sampler = pipe['loader_settings']['sampler']
sigmas = pipe['loader_settings']['sigmas']
cfg = pipe['loader_settings']['cfg']
steps = pipe['loader_settings']['steps']
disable_noise = False
preview_latent = True
if image_output in ("Hide", "Hide/Save"):
preview_latent = False
# 推理初始时间
start_time = int(time.time() * 1000)
# 开始推理
samp_samples = sampler.custom_ksampler(samp_model, samp_seed, steps, cfg, samp_sampler, sigmas, samp_positive, samp_negative, samp_samples,
disable_noise, preview_latent)
# 推理结束时间
end_time = int(time.time() * 1000)
latent = samp_samples['samples']
# 解码图片
if tile_size is not None:
samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, )
else:
samp_images = samp_vae.decode(latent).cpu()
# 推理总耗时(包含解码)
end_decode_time = int(time.time() * 1000)
spent_time = '扩散:' + str((end_time - start_time) / 1000) + '秒, 解码:' + str(
(end_decode_time - end_time) / 1000) + '秒'
# Clean loaded_objects
easyCache.update_loaded_objects(prompt)
results = easy_save.images(samp_images, save_prefix, image_output)
sampler.update_value_by_id("results", my_unique_id, results)
new_pipe = {
"model": samp_model,
"positive": samp_positive,
"negative": samp_negative,
"vae": samp_vae,
"clip": samp_clip,
"samples": samp_samples,
"images": samp_images,
"seed": samp_seed,
"loader_settings": {
**pipe["loader_settings"],
"spent_time": spent_time
}
}
sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe)
del pipe
if image_output in ("Hide", "Hide/Save"):
return {"ui": {},
"result": sampler.get_output(new_pipe, )}
if image_output in ("Sender", "Sender/Save"):
PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results})
return {"ui": {"images": results},
"result": sampler.get_output(new_pipe, )}
class unsampler:
@classmethod
def INPUT_TYPES(s):
return {"required":{
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"end_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"normalize": (["disable", "enable"],),
},
"optional": {
"pipe": ("PIPE_LINE",),
"optional_model": ("MODEL",),
"optional_positive": ("CONDITIONING",),
"optional_negative": ("CONDITIONING",),
"optional_latent": ("LATENT",),
}
}
RETURN_TYPES = ("PIPE_LINE", "LATENT",)
RETURN_NAMES = ("pipe", "latent",)
FUNCTION = "unsampler"
CATEGORY = "EasyUse/Sampler"
def unsampler(self, cfg, sampler_name, steps, end_at_step, scheduler, normalize, pipe=None, optional_model=None, optional_positive=None, optional_negative=None,
optional_latent=None):
model = optional_model if optional_model is not None else pipe["model"]
positive = optional_positive if optional_positive is not None else pipe["positive"]
negative = optional_negative if optional_negative is not None else pipe["negative"]
latent_image = optional_latent if optional_latent is not None else pipe["samples"]
normalize = normalize == "enable"
device = comfy.model_management.get_torch_device()
latent = latent_image
latent_image = latent["samples"]
end_at_step = min(end_at_step, steps - 1)
end_at_step = steps - end_at_step
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
noise_mask = None
if "noise_mask" in latent:
noise_mask = comfy.sample.prepare_mask(latent["noise_mask"], noise.shape, device)
real_model = None
real_model = model.model
noise = noise.to(device)
latent_image = latent_image.to(device)
positive = comfy.sample.convert_cond(positive)
negative = comfy.sample.convert_cond(negative)
models, inference_memory = comfy.sample.get_additional_models(positive, negative, model.model_dtype())
comfy.model_management.load_models_gpu([model] + models, model.memory_required(noise.shape) + inference_memory)
sampler = comfy.samplers.KSampler(real_model, steps=steps, device=device, sampler=sampler_name,
scheduler=scheduler, denoise=1.0, model_options=model.model_options)
sigmas = sigmas = sampler.sigmas.flip(0) + 0.0001
pbar = comfy.utils.ProgressBar(steps)
def callback(step, x0, x, total_steps):
pbar.update_absolute(step + 1, total_steps)
samples = sampler.sample(noise, positive, negative, cfg=cfg, latent_image=latent_image,
force_full_denoise=False, denoise_mask=noise_mask, sigmas=sigmas, start_step=0,
last_step=end_at_step, callback=callback)
if normalize:
# technically doesn't normalize because unsampling is not guaranteed to end at a std given by the schedule
samples -= samples.mean()
samples /= samples.std()
samples = samples.cpu()
comfy.sample.cleanup_additional_models(models)
out = latent.copy()
out["samples"] = samples
if pipe is None:
pipe = {}
new_pipe = {
**pipe,
"samples": out
}
return (new_pipe, out,)
#---------------------------------------------------------------修复 开始----------------------------------------------------------------------#
# 高清修复
class hiresFix:
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos", "bislerp"]
crop_methods = ["disabled", "center"]
@classmethod
def INPUT_TYPES(s):
return {"required": {
"model_name": (folder_paths.get_filename_list("upscale_models"),),
"rescale_after_model": ([False, True], {"default": True}),
"rescale_method": (s.upscale_methods,),
"rescale": (["by percentage", "to Width/Height", 'to longer side - maintain aspect'],),
"percent": ("INT", {"default": 50, "min": 0, "max": 1000, "step": 1}),
"width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"longer_side": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"crop": (s.crop_methods,),
"image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],{"default": "Preview"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"pipe": ("PIPE_LINE",),
"image": ("IMAGE",),
"vae": ("VAE",),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = ("PIPE_LINE", "IMAGE", "LATENT", )
RETURN_NAMES = ('pipe', 'image', "latent", )
FUNCTION = "upscale"
CATEGORY = "EasyUse/Fix"
OUTPUT_NODE = True
def vae_encode_crop_pixels(self, pixels):
x = (pixels.shape[1] // 8) * 8
y = (pixels.shape[2] // 8) * 8
if pixels.shape[1] != x or pixels.shape[2] != y:
x_offset = (pixels.shape[1] % 8) // 2
y_offset = (pixels.shape[2] % 8) // 2
pixels = pixels[:, x_offset:x + x_offset, y_offset:y + y_offset, :]
return pixels
def upscale(self, model_name, rescale_after_model, rescale_method, rescale, percent, width, height,
longer_side, crop, image_output, link_id, save_prefix, pipe=None, image=None, vae=None, prompt=None,
extra_pnginfo=None, my_unique_id=None):
new_pipe = {}
if pipe is not None:
image = image if image is not None else pipe["images"]
vae = vae if vae is not None else pipe.get("vae")
elif image is None or vae is None:
raise ValueError("pipe or image or vae missing.")
# Load Model
model_path = folder_paths.get_full_path("upscale_models", model_name)
sd = comfy.utils.load_torch_file(model_path, safe_load=True)
upscale_model = model_loading.load_state_dict(sd).eval()
# Model upscale
device = comfy.model_management.get_torch_device()
upscale_model.to(device)
in_img = image.movedim(-1, -3).to(device)
tile = 128 + 64
overlap = 8
steps = in_img.shape[0] * comfy.utils.get_tiled_scale_steps(in_img.shape[3], in_img.shape[2], tile_x=tile,
tile_y=tile, overlap=overlap)
pbar = comfy.utils.ProgressBar(steps)
s = comfy.utils.tiled_scale(in_img, lambda a: upscale_model(a), tile_x=tile, tile_y=tile, overlap=overlap,
upscale_amount=upscale_model.scale, pbar=pbar)
upscale_model.cpu()
s = torch.clamp(s.movedim(-3, -1), min=0, max=1.0)
# Post Model Rescale
if rescale_after_model == True:
samples = s.movedim(-1, 1)
orig_height = samples.shape[2]
orig_width = samples.shape[3]
if rescale == "by percentage" and percent != 0:
height = percent / 100 * orig_height
width = percent / 100 * orig_width
if (width > MAX_RESOLUTION):
width = MAX_RESOLUTION
if (height > MAX_RESOLUTION):
height = MAX_RESOLUTION
width = easySampler.enforce_mul_of_64(width)
height = easySampler.enforce_mul_of_64(height)
elif rescale == "to longer side - maintain aspect":
longer_side = easySampler.enforce_mul_of_64(longer_side)
if orig_width > orig_height:
width, height = longer_side, easySampler.enforce_mul_of_64(longer_side * orig_height / orig_width)
else:
width, height = easySampler.enforce_mul_of_64(longer_side * orig_width / orig_height), longer_side
s = comfy.utils.common_upscale(samples, width, height, rescale_method, crop)
s = s.movedim(1, -1)
# vae encode
pixels = self.vae_encode_crop_pixels(s)
t = vae.encode(pixels[:, :, :, :3])
if pipe is not None:
new_pipe = {
"model": pipe['model'],
"positive": pipe['positive'],
"negative": pipe['negative'],
"vae": vae,
"clip": pipe['clip'],
"samples": {"samples": t},
"images": s,
"seed": pipe['seed'],
"loader_settings": {
**pipe["loader_settings"],
}
}
del pipe
else:
new_pipe = {}
easy_save = easySave(my_unique_id, prompt, extra_pnginfo)
results = easy_save.images(s, save_prefix, image_output)
if image_output in ("Sender", "Sender/Save"):
PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results})
if image_output in ("Hide", "Hide/Save"):
return (new_pipe, s, {"samples": t},)
return {"ui": {"images": results},
"result": (new_pipe, s, {"samples": t},)}
# 预细节修复
class preDetailerFix:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"pipe": ("PIPE_LINE",),
"guide_size": ("FLOAT", {"default": 256, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"guide_size_for": ("BOOLEAN", {"default": True, "label_on": "bbox", "label_off": "crop_region"}),
"max_size": ("FLOAT", {"default": 768, "min": 64, "max": MAX_RESOLUTION, "step": 8}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
"denoise": ("FLOAT", {"default": 0.5, "min": 0.0001, "max": 1.0, "step": 0.01}),
"feather": ("INT", {"default": 5, "min": 0, "max": 100, "step": 1}),
"noise_mask": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"force_inpaint": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"drop_size": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 1, "default": 10}),
"wildcard": ("STRING", {"multiline": True, "dynamicPrompts": False}),
"cycle": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}),
},
"optional": {
"bbox_segm_pipe": ("PIPE_LINE",),
"sam_pipe": ("PIPE_LINE",),
"optional_image": ("IMAGE",),
},
}
RETURN_TYPES = ("PIPE_LINE",)
RETURN_NAMES = ("pipe",)
OUTPUT_IS_LIST = (False,)
FUNCTION = "doit"
CATEGORY = "EasyUse/Fix"
def doit(self, pipe, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler, denoise, feather, noise_mask, force_inpaint, drop_size, wildcard, cycle, bbox_segm_pipe=None, sam_pipe=None, optional_image=None):
model = pipe["model"] if "model" in pipe else None
if model is None:
raise Exception(f"[ERROR] pipe['model'] is missing")
clip = pipe["clip"] if"clip" in pipe else None
if clip is None:
raise Exception(f"[ERROR] pipe['clip'] is missing")
vae = pipe["vae"] if "vae" in pipe else None
if vae is None:
raise Exception(f"[ERROR] pipe['vae'] is missing")
if optional_image is not None:
batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1
samples = {"samples": vae.encode(optional_image)}
samples = RepeatLatentBatch().repeat(samples, batch_size)[0]
image = optional_image
else:
samples = pipe["samples"] if "samples" in pipe else None
if samples is None:
raise Exception(f"[ERROR] pipe['samples'] is missing")
image = pipe["images"] if "images" in pipe else None
if image is None:
raise Exception(f"[ERROR] pipe['image'] is missing")
positive = pipe["positive"] if "positive" in pipe else None
if positive is None:
raise Exception(f"[ERROR] pipe['positive'] is missing")
negative = pipe["negative"] if "negative" in pipe else None
if negative is None:
raise Exception(f"[ERROR] pipe['negative'] is missing")
bbox_segm_pipe = bbox_segm_pipe or (pipe["bbox_segm_pipe"] if pipe and "bbox_segm_pipe" in pipe else None)
if bbox_segm_pipe is None:
raise Exception(f"[ERROR] bbox_segm_pipe or pipe['bbox_segm_pipe'] is missing")
sam_pipe = sam_pipe or (pipe["sam_pipe"] if pipe and "sam_pipe" in pipe else None)
if sam_pipe is None:
raise Exception(f"[ERROR] sam_pipe or pipe['sam_pipe'] is missing")
loader_settings = pipe["loader_settings"] if "loader_settings" in pipe else {}
new_pipe = {
"samples": samples,
"images": image,
"model": model,
"clip": clip,
"vae": vae,
"positive": positive,
"negative": negative,
"seed": seed,
"bbox_segm_pipe": bbox_segm_pipe,
"sam_pipe": sam_pipe,
"loader_settings": loader_settings,
"detail_fix_settings": {
"guide_size": guide_size,
"guide_size_for": guide_size_for,
"max_size": max_size,
"seed": seed,
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": denoise,
"feather": feather,
"noise_mask": noise_mask,
"force_inpaint": force_inpaint,
"drop_size": drop_size,
"wildcard": wildcard,
"cycle": cycle
}
}
del bbox_segm_pipe
del sam_pipe
return (new_pipe,)
# 细节修复
class detailerFix:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"pipe": ("PIPE_LINE",),
"image_output": (["Hide", "Preview", "Save", "Hide/Save", "Sender", "Sender/Save"],{"default": "Preview"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"model": ("MODEL",),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", }
}
RETURN_TYPES = ("PIPE_LINE", "IMAGE",)
RETURN_NAMES = ("pipe", "image")
OUTPUT_NODE = True
OUTPUT_IS_LIST = (False, False)
FUNCTION = "doit"
CATEGORY = "EasyUse/Fix"
def doit(self, pipe, image_output, link_id, save_prefix, model=None, prompt=None, extra_pnginfo=None, my_unique_id=None):
# Clean loaded_objects
easyCache.update_loaded_objects(prompt)
my_unique_id = int(my_unique_id)
easy_save = easySave(my_unique_id, prompt, extra_pnginfo)
model = model or (pipe["model"] if "model" in pipe else None)
if model is None:
raise Exception(f"[ERROR] model or pipe['model'] is missing")
bbox_segm_pipe = pipe["bbox_segm_pipe"] if pipe and "bbox_segm_pipe" in pipe else None
if bbox_segm_pipe is None:
raise Exception(f"[ERROR] bbox_segm_pipe or pipe['bbox_segm_pipe'] is missing")
sam_pipe = pipe["sam_pipe"] if "sam_pipe" in pipe else None
if sam_pipe is None:
raise Exception(f"[ERROR] sam_pipe or pipe['sam_pipe'] is missing")
bbox_detector_opt, bbox_threshold, bbox_dilation, bbox_crop_factor, segm_detector_opt = bbox_segm_pipe
sam_model_opt, sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold, sam_mask_hint_use_negative = sam_pipe
detail_fix_settings = pipe["detail_fix_settings"] if "detail_fix_settings" in pipe else None
if detail_fix_settings is None:
raise Exception(f"[ERROR] detail_fix_settings or pipe['detail_fix_settings'] is missing")
image = pipe["images"]
clip = pipe["clip"]
vae = pipe["vae"]
seed = pipe["seed"]
positive = pipe["positive"]
negative = pipe["negative"]
loader_settings = pipe["loader_settings"] if "loader_settings" in pipe else {}
guide_size = pipe["detail_fix_settings"]["guide_size"]
guide_size_for = pipe["detail_fix_settings"]["guide_size_for"]
max_size = pipe["detail_fix_settings"]["max_size"]
steps = pipe["detail_fix_settings"]["steps"]
cfg = pipe["detail_fix_settings"]["cfg"]
sampler_name = pipe["detail_fix_settings"]["sampler_name"]
scheduler = pipe["detail_fix_settings"]["scheduler"]
denoise = pipe["detail_fix_settings"]["denoise"]
feather = pipe["detail_fix_settings"]["feather"]
noise_mask = pipe["detail_fix_settings"]["noise_mask"]
force_inpaint = pipe["detail_fix_settings"]["force_inpaint"]
drop_size = pipe["detail_fix_settings"]["drop_size"]
wildcard = pipe["detail_fix_settings"]["wildcard"]
cycle = pipe["detail_fix_settings"]["cycle"]
del pipe
# 细节修复初始时间
start_time = int(time.time() * 1000)
cls = ALL_NODE_CLASS_MAPPINGS["FaceDetailer"]
enhanced_img, cropped_enhanced, cropped_enhanced_alpha, mask, cnet_pil_list = cls().enhance_face(
image, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name,
scheduler,
positive, negative, denoise, feather, noise_mask, force_inpaint,
bbox_threshold, bbox_dilation, bbox_crop_factor,
sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold,
sam_mask_hint_use_negative, drop_size, bbox_detector_opt, segm_detector_opt, sam_model_opt, wildcard,
detailer_hook=None, cycle=cycle)
# 细节修复结束时间
end_time = int(time.time() * 1000)
spent_time = '细节修复:' + str((end_time - start_time) / 1000) + '秒'
results = easy_save.images(enhanced_img, save_prefix, image_output)
sampler.update_value_by_id("results", my_unique_id, results)
# Clean loaded_objects
easyCache.update_loaded_objects(prompt)
new_pipe = {
"samples": None,
"images": enhanced_img,
"model": model,
"clip": clip,
"vae": vae,
"seed": seed,
"positive": positive,
"negative": negative,
"wildcard": wildcard,
"bbox_segm_pipe": bbox_segm_pipe,
"sam_pipe": sam_pipe,
"loader_settings": {
**loader_settings,
"spent_time": spent_time
},
"detail_fix_settings": detail_fix_settings
}
sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe)
del bbox_segm_pipe
del sam_pipe
if image_output in ("Hide", "Hide/Save"):
return {"ui": {},
"result": (new_pipe, enhanced_img, cropped_enhanced, cropped_enhanced_alpha, mask, cnet_pil_list)}
if image_output in ("Sender", "Sender/Save"):
PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results})
return {"ui": {"images": results}, "result": (new_pipe, enhanced_img, cropped_enhanced, cropped_enhanced_alpha, mask, cnet_pil_list)}
def add_folder_path_and_extensions(folder_name, full_folder_paths, extensions):
for full_folder_path in full_folder_paths:
folder_paths.add_model_folder_path(folder_name, full_folder_path)
if folder_name in folder_paths.folder_names_and_paths:
current_paths, current_extensions = folder_paths.folder_names_and_paths[folder_name]
updated_extensions = current_extensions | extensions
folder_paths.folder_names_and_paths[folder_name] = (current_paths, updated_extensions)
else:
folder_paths.folder_names_and_paths[folder_name] = (full_folder_paths, extensions)
model_path = folder_paths.models_dir
add_folder_path_and_extensions("ultralytics_bbox", [os.path.join(model_path, "ultralytics", "bbox")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("ultralytics_segm", [os.path.join(model_path, "ultralytics", "segm")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("ultralytics", [os.path.join(model_path, "ultralytics")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("mmdets_bbox", [os.path.join(model_path, "mmdets", "bbox")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("mmdets_segm", [os.path.join(model_path, "mmdets", "segm")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("mmdets", [os.path.join(model_path, "mmdets")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("sams", [os.path.join(model_path, "sams")], folder_paths.supported_pt_extensions)
add_folder_path_and_extensions("onnx", [os.path.join(model_path, "onnx")], {'.onnx'})
class ultralyticsDetectorForDetailerFix:
@classmethod
def INPUT_TYPES(s):
bboxs = ["bbox/" + x for x in folder_paths.get_filename_list("ultralytics_bbox")]
segms = ["segm/" + x for x in folder_paths.get_filename_list("ultralytics_segm")]
return {"required":
{"model_name": (bboxs + segms,),
"bbox_threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"bbox_dilation": ("INT", {"default": 10, "min": -512, "max": 512, "step": 1}),
"bbox_crop_factor": ("FLOAT", {"default": 3.0, "min": 1.0, "max": 10, "step": 0.1}),
}
}
RETURN_TYPES = ("PIPE_LINE",)
RETURN_NAMES = ("bbox_segm_pipe",)
FUNCTION = "doit"
CATEGORY = "EasyUse/Fix"
def doit(self, model_name, bbox_threshold, bbox_dilation, bbox_crop_factor):
if 'UltralyticsDetectorProvider' not in ALL_NODE_CLASS_MAPPINGS:
raise Exception(f"[ERROR] To use UltralyticsDetectorProvider, you need to install 'Impact Pack'")
cls = ALL_NODE_CLASS_MAPPINGS['UltralyticsDetectorProvider']
bbox_detector, segm_detector = cls().doit(model_name)
pipe = (bbox_detector, bbox_threshold, bbox_dilation, bbox_crop_factor, segm_detector)
return (pipe,)
class samLoaderForDetailerFix:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_name": (folder_paths.get_filename_list("sams"),),
"device_mode": (["AUTO", "Prefer GPU", "CPU"],{"default": "AUTO"}),
"sam_detection_hint": (
["center-1", "horizontal-2", "vertical-2", "rect-4", "diamond-4", "mask-area", "mask-points",
"mask-point-bbox", "none"],),
"sam_dilation": ("INT", {"default": 0, "min": -512, "max": 512, "step": 1}),
"sam_threshold": ("FLOAT", {"default": 0.93, "min": 0.0, "max": 1.0, "step": 0.01}),
"sam_bbox_expansion": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}),
"sam_mask_hint_threshold": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01}),
"sam_mask_hint_use_negative": (["False", "Small", "Outter"],),
}
}
RETURN_TYPES = ("PIPE_LINE",)
RETURN_NAMES = ("sam_pipe",)
FUNCTION = "doit"
CATEGORY = "EasyUse/Fix"
def doit(self, model_name, device_mode, sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold, sam_mask_hint_use_negative):
if 'SAMLoader' not in ALL_NODE_CLASS_MAPPINGS:
raise Exception(f"[ERROR] To use SAMLoader, you need to install 'Impact Pack'")
cls = ALL_NODE_CLASS_MAPPINGS['SAMLoader']
(sam_model,) = cls().load_model(model_name, device_mode)
pipe = (sam_model, sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold, sam_mask_hint_use_negative)
return (pipe,)
#---------------------------------------------------------------Pipe 开始----------------------------------------------------------------------#
# pipeIn
class pipeIn:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required":{
"pipe": ("PIPE_LINE",),
},
"optional": {
"model": ("MODEL",),
"pos": ("CONDITIONING",),
"neg": ("CONDITIONING",),
"latent": ("LATENT",),
"vae": ("VAE",),
"clip": ("CLIP",),
"image": ("IMAGE",),
"xyPlot": ("XYPLOT",),
},
"hidden": {"my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("PIPE_LINE",)
RETURN_NAMES = ("pipe",)
FUNCTION = "flush"
CATEGORY = "EasyUse/Pipe"
def flush(self, pipe, model=None, pos=None, neg=None, latent=None, vae=None, clip=None, image=None, xyplot=None, my_unique_id=None):
model = model if model is not None else pipe.get("model")
if model is None:
log_node_warn(f'pipeIn[{my_unique_id}]', "Model missing from pipeLine")
pos = pos if pos is not None else pipe.get("positive")
if pos is None:
log_node_warn(f'pipeIn[{my_unique_id}]', "Pos Conditioning missing from pipeLine")
neg = neg if neg is not None else pipe.get("negative")
if neg is None:
log_node_warn(f'pipeIn[{my_unique_id}]', "Neg Conditioning missing from pipeLine")
samples = latent if latent is not None else pipe.get("samples")
if samples is None:
log_node_warn(f'pipeIn[{my_unique_id}]', "Latent missing from pipeLine")
vae = vae if vae is not None else pipe.get("vae")
if vae is None:
log_node_warn(f'pipeIn[{my_unique_id}]', "VAE missing from pipeLine")
clip = clip if clip is not None else pipe.get("clip")
if clip is None:
log_node_warn(f'pipeIn[{my_unique_id}]', "Clip missing from pipeLine")
if image is None:
image = pipe.get("images")
else:
batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1
samples = {"samples": vae.encode(image)}
samples = RepeatLatentBatch().repeat(samples, batch_size)[0]
seed = pipe.get("seed")
if seed is None:
log_node_warn(f'pipeIn[{my_unique_id}]', "Seed missing from pipeLine")
xyplot = xyplot or pipe['loader_settings']['xyplot'] if 'xyplot' in pipe['loader_settings'] else None
new_pipe = {
"model": model,
"positive": pos,
"negative": neg,
"vae": vae,
"clip": clip,
"samples": samples,
"images": image,
"seed": seed,
"loader_settings": {
**pipe["loader_settings"],
"positive": "",
"negative": "",
"xyplot": xyplot
}
}
del pipe
return (new_pipe,)
# pipeOut
class pipeOut:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pipe": ("PIPE_LINE",),
},
"hidden": {"my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("PIPE_LINE", "MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "IMAGE", "INT",)
RETURN_NAMES = ("pipe", "model", "pos", "neg", "latent", "vae", "clip", "image", "seed",)
FUNCTION = "flush"
CATEGORY = "EasyUse/Pipe"
def flush(self, pipe, my_unique_id=None):
model = pipe.get("model")
pos = pipe.get("positive")
neg = pipe.get("negative")
latent = pipe.get("samples")
vae = pipe.get("vae")
clip = pipe.get("clip")
image = pipe.get("images")
seed = pipe.get("seed")
return pipe, model, pos, neg, latent, vae, clip, image, seed
# pipeToBasicPipe
class pipeToBasicPipe:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pipe": ("PIPE_LINE",),
},
"hidden": {"my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("BASIC_PIPE",)
RETURN_NAMES = ("basic_pipe",)
FUNCTION = "doit"
CATEGORY = "EasyUse/Pipe"
def doit(self, pipe, my_unique_id=None):
new_pipe = (pipe.get('model'), pipe.get('clip'), pipe.get('vae'), pipe.get('positive'), pipe.get('negative'))
del pipe
return (new_pipe,)
# pipeXYPlot
class pipeXYPlot:
lora_list = ["None"] + folder_paths.get_filename_list("loras")
lora_strengths = {"min": -4.0, "max": 4.0, "step": 0.01}
token_normalization = ["none", "mean", "length", "length+mean"]
weight_interpretation = ["comfy", "A1111", "compel", "comfy++"]
loader_dict = {
"ckpt_name": folder_paths.get_filename_list("checkpoints"),
"vae_name": ["Baked-VAE"] + folder_paths.get_filename_list("vae"),
"clip_skip": {"min": -24, "max": -1, "step": 1},
"lora_name": lora_list,
"lora_model_strength": lora_strengths,
"lora_clip_strength": lora_strengths,
"positive": [],
"negative": [],
}
sampler_dict = {
"steps": {"min": 1, "max": 100, "step": 1},
"cfg": {"min": 0.0, "max": 100.0, "step": 1.0},
"sampler_name": comfy.samplers.KSampler.SAMPLERS,
"scheduler": comfy.samplers.KSampler.SCHEDULERS,
"denoise": {"min": 0.0, "max": 1.0, "step": 0.01},
"seed": {"min": 0, "max": 1125899906842624},
}
plot_dict = {**sampler_dict, **loader_dict}
plot_values = ["None", ]
plot_values.append("---------------------")
for k in sampler_dict:
plot_values.append(f'preSampling: {k}')
plot_values.append("---------------------")
for k in loader_dict:
plot_values.append(f'loader: {k}')
def __init__(self):
pass
rejected = ["None", "---------------------", "Nothing"]
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"grid_spacing": ("INT", {"min": 0, "max": 500, "step": 5, "default": 0, }),
"output_individuals": (["False", "True"], {"default": "False"}),
"flip_xy": (["False", "True"], {"default": "False"}),
"x_axis": (pipeXYPlot.plot_values, {"default": 'None'}),
"x_values": (
"STRING", {"default": '', "multiline": True, "placeholder": 'insert values seperated by "; "'}),
"y_axis": (pipeXYPlot.plot_values, {"default": 'None'}),
"y_values": (
"STRING", {"default": '', "multiline": True, "placeholder": 'insert values seperated by "; "'}),
},
"optional": {
"pipe": ("PIPE_LINE",)
},
"hidden": {
"plot_dict": (pipeXYPlot.plot_dict,),
},
}
RETURN_TYPES = ("PIPE_LINE",)
RETURN_NAMES = ("pipe",)
FUNCTION = "plot"
CATEGORY = "EasyUse/Pipe"
def plot(self, grid_spacing, output_individuals, flip_xy, x_axis, x_values, y_axis, y_values, pipe=None):
def clean_values(values):
original_values = values.split("; ")
cleaned_values = []
for value in original_values:
# Strip the semi-colon
cleaned_value = value.strip(';').strip()
if cleaned_value == "":
continue
# Try to convert the cleaned_value back to int or float if possible
try:
cleaned_value = int(cleaned_value)
except ValueError:
try:
cleaned_value = float(cleaned_value)
except ValueError:
pass
# Append the cleaned_value to the list
cleaned_values.append(cleaned_value)
return cleaned_values
if x_axis in self.rejected:
x_axis = "None"
x_values = []
else:
x_values = clean_values(x_values)
if y_axis in self.rejected:
y_axis = "None"
y_values = []
else:
y_values = clean_values(y_values)
if flip_xy == "True":
x_axis, y_axis = y_axis, x_axis
x_values, y_values = y_values, x_values
xy_plot = {"x_axis": x_axis,
"x_vals": x_values,
"y_axis": y_axis,
"y_vals": y_values,
"grid_spacing": grid_spacing,
"output_individuals": output_individuals}
if pipe is not None:
new_pipe = pipe
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
"xyplot": xy_plot
}
del pipe
return (new_pipe, xy_plot,)
# pipeXYPlotAdvanced
class pipeXYPlotAdvanced:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pipe": ("PIPE_LINE",),
"grid_spacing": ("INT", {"min": 0, "max": 500, "step": 5, "default": 0, }),
"output_individuals": (["False", "True"], {"default": "False"}),
"flip_xy": (["False", "True"], {"default": "False"}),
},
"optional": {
"X": ("X_Y",),
"Y": ("X_Y",),
},
"hidden": {"my_unique_id": "UNIQUE_ID"}
}
RETURN_TYPES = ("PIPE_LINE",)
RETURN_NAMES = ("pipe",)
FUNCTION = "plot"
CATEGORY = "EasyUse/Pipe"
def plot(self, pipe, grid_spacing, output_individuals, flip_xy, X=None, Y=None, my_unique_id=None):
if X != None:
x_axis = X.get('axis')
x_values = X.get('values')
else:
x_axis = "Nothing"
x_values = [""]
if Y != None:
y_axis = Y.get('axis')
y_values = Y.get('values')
else:
y_axis = "Nothing"
y_values = [""]
if pipe is not None:
new_pipe = pipe
positive = pipe["loader_settings"]["positive"] if "positive" in pipe["loader_settings"] else ""
negative = pipe["loader_settings"]["negative"] if "negative" in pipe["loader_settings"] else ""
if x_axis == 'advanced: ModelMergeBlocks':
models = X.get('models')
vae_use = X.get('vae_use')
if models is None:
raise Exception("models is not found")
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
"models": models,
"vae_use": vae_use
}
if y_axis == 'advanced: ModelMergeBlocks':
models = Y.get('models')
vae_use = Y.get('vae_use')
if models is None:
raise Exception("models is not found")
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
"models": models,
"vae_use": vae_use
}
if x_axis == 'advanced: Seeds++ Batch':
if new_pipe['seed']:
value = x_values
x_values = []
for index in range(value):
x_values.append(str(new_pipe['seed'] + index))
x_values = "; ".join(x_values)
if y_axis == 'advanced: Seeds++ Batch':
if new_pipe['seed']:
value = y_values
y_values = []
for index in range(value):
y_values.append(str(new_pipe['seed'] + index))
y_values = "; ".join(y_values)
if x_axis == 'advanced: Positive Prompt S/R':
if positive:
x_value = x_values
x_values = []
for index, value in enumerate(x_value):
search_txt, replace_txt, replace_all = value
if replace_all:
txt = replace_txt if replace_txt is not None else positive
x_values.append(txt)
else:
txt = positive.replace(search_txt, replace_txt, 1) if replace_txt is not None else positive
x_values.append(txt)
x_values = "; ".join(x_values)
if y_axis == 'advanced: Positive Prompt S/R':
if positive:
y_value = y_values
y_values = []
for index, value in enumerate(y_value):
search_txt, replace_txt, replace_all = value
if replace_all:
txt = replace_txt if replace_txt is not None else positive
y_values.append(txt)
else:
txt = positive.replace(search_txt, replace_txt, 1) if replace_txt is not None else positive
y_values.append(txt)
y_values = "; ".join(y_values)
if x_axis == 'advanced: Negative Prompt S/R':
if negative:
x_value = x_values
x_values = []
for index, value in enumerate(x_value):
search_txt, replace_txt, replace_all = value
if replace_all:
txt = replace_txt if replace_txt is not None else negative
x_values.append(txt)
else:
txt = negative.replace(search_txt, replace_txt, 1) if replace_txt is not None else negative
x_values.append(txt)
x_values = "; ".join(x_values)
if y_axis == 'advanced: Negative Prompt S/R':
if negative:
y_value = y_values
y_values = []
for index, value in enumerate(y_value):
search_txt, replace_txt, replace_all = value
if replace_all:
txt = replace_txt if replace_txt is not None else negative
y_values.append(txt)
else:
txt = negative.replace(search_txt, replace_txt, 1) if replace_txt is not None else negative
y_values.append(txt)
y_values = "; ".join(y_values)
if "advanced: ControlNet" in x_axis:
x_value = x_values
x_values = []
cnet = []
for index, value in enumerate(x_value):
cnet.append(value)
x_values.append(str(index))
x_values = "; ".join(x_values)
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
"cnet_stack": cnet,
}
if "advanced: ControlNet" in y_axis:
y_value = y_values
y_values = []
cnet = []
for index, value in enumerate(y_value):
cnet.append(value)
y_values.append(str(index))
y_values = "; ".join(y_values)
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
"cnet_stack": cnet,
}
if "advanced: Pos Condition" in x_axis:
x_values = "; ".join(x_values)
cond = X.get('cond')
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
"positive_cond_stack": cond,
}
if "advanced: Pos Condition" in y_axis:
y_values = "; ".join(y_values)
cond = Y.get('cond')
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
"positive_cond_stack": cond,
}
if "advanced: Neg Condition" in x_axis:
x_values = "; ".join(x_values)
cond = X.get('cond')
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
"negative_cond_stack": cond,
}
if "advanced: Neg Condition" in y_axis:
y_values = "; ".join(y_values)
cond = Y.get('cond')
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
"negative_cond_stack": cond,
}
del pipe
return pipeXYPlot().plot(grid_spacing, output_individuals, flip_xy, x_axis, x_values, y_axis, y_values, new_pipe)
#---------------------------------------------------------------XY Inputs 开始----------------------------------------------------------------------#
def load_preset(filename):
path = os.path.join(RESOURCES_DIR, filename)
path = os.path.abspath(path)
preset_list = []
if os.path.exists(path):
with open(path, 'r') as file:
for line in file:
preset_list.append(line.strip())
return preset_list
else:
return []
def generate_floats(batch_count, first_float, last_float):
if batch_count > 1:
interval = (last_float - first_float) / (batch_count - 1)
values = [str(round(first_float + i * interval, 3)) for i in range(batch_count)]
else:
values = [str(first_float)] if batch_count == 1 else []
return "; ".join(values)
def generate_ints(batch_count, first_int, last_int):
if batch_count > 1:
interval = (last_int - first_int) / (batch_count - 1)
values = [str(int(first_int + i * interval)) for i in range(batch_count)]
else:
values = [str(first_int)] if batch_count == 1 else []
# values = list(set(values)) # Remove duplicates
# values.sort() # Sort in ascending order
return "; ".join(values)
# Seed++ Batch
class XYplot_SeedsBatch:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"batch_count": ("INT", {"default": 3, "min": 1, "max": 50}), },
}
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, batch_count):
axis = "advanced: Seeds++ Batch"
xy_values = {"axis": axis, "values": batch_count}
return (xy_values,)
# Step Values
class XYplot_Steps:
parameters = ["steps", "start_at_step", "end_at_step",]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"target_parameter": (cls.parameters,),
"batch_count": ("INT", {"default": 3, "min": 0, "max": 50}),
"first_step": ("INT", {"default": 10, "min": 1, "max": 10000}),
"last_step": ("INT", {"default": 20, "min": 1, "max": 10000}),
"first_start_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
"last_start_step": ("INT", {"default": 10, "min": 0, "max": 10000}),
"first_end_step": ("INT", {"default": 10, "min": 0, "max": 10000}),
"last_end_step": ("INT", {"default": 20, "min": 0, "max": 10000}),
}
}
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, target_parameter, batch_count, first_step, last_step, first_start_step, last_start_step,
first_end_step, last_end_step,):
axis, xy_first, xy_last = None, None, None
if target_parameter == "steps":
axis = "advanced: Steps"
xy_first = first_step
xy_last = last_step
elif target_parameter == "start_at_step":
axis = "advanced: StartStep"
xy_first = first_start_step
xy_last = last_start_step
elif target_parameter == "end_at_step":
axis = "advanced: EndStep"
xy_first = first_end_step
xy_last = last_end_step
values = generate_ints(batch_count, xy_first, xy_last)
return ({"axis": axis, "values": values},) if values is not None else (None,)
class XYplot_CFG:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"batch_count": ("INT", {"default": 3, "min": 0, "max": 50}),
"first_cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}),
"last_cfg": ("FLOAT", {"default": 9.0, "min": 0.0, "max": 100.0}),
}
}
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, batch_count, first_cfg, last_cfg):
axis = "advanced: CFG Scale"
values = generate_floats(batch_count, first_cfg, last_cfg)
return ({"axis": axis, "values": values},) if values else (None,)
# Step Values
class XYplot_Sampler_Scheduler:
parameters = ["sampler", "scheduler", "sampler & scheduler"]
@classmethod
def INPUT_TYPES(cls):
samplers = ["None"] + comfy.samplers.KSampler.SAMPLERS
schedulers = ["None"] + comfy.samplers.KSampler.SCHEDULERS
inputs = {
"required": {
"target_parameter": (cls.parameters,),
"input_count": ("INT", {"default": 1, "min": 1, "max": 30, "step": 1})
}
}
for i in range(1, 30 + 1):
inputs["required"][f"sampler_{i}"] = (samplers,)
inputs["required"][f"scheduler_{i}"] = (schedulers,)
return inputs
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, target_parameter, input_count, **kwargs):
axis, values, = None, None,
if target_parameter == "scheduler":
axis = "advanced: Scheduler"
schedulers = [kwargs.get(f"scheduler_{i}") for i in range(1, input_count + 1)]
values = [scheduler for scheduler in schedulers if scheduler != "None"]
elif target_parameter == "sampler":
axis = "advanced: Sampler"
samplers = [kwargs.get(f"sampler_{i}") for i in range(1, input_count + 1)]
values = [sampler for sampler in samplers if sampler != "None"]
else:
axis = "advanced: Sampler&Scheduler"
samplers = [kwargs.get(f"sampler_{i}") for i in range(1, input_count + 1)]
schedulers = [kwargs.get(f"scheduler_{i}") for i in range(1, input_count + 1)]
values = []
for sampler, scheduler in zip(samplers, schedulers):
sampler = sampler if sampler else 'None'
scheduler = scheduler if scheduler else 'None'
values.append(sampler +', '+ scheduler)
values = "; ".join(values)
return ({"axis": axis, "values": values},) if values else (None,)
class XYplot_Denoise:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"batch_count": ("INT", {"default": 3, "min": 0, "max": 50}),
"first_denoise": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}),
"last_denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}),
}
}
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, batch_count, first_denoise, last_denoise):
axis = "advanced: Denoise"
values = generate_floats(batch_count, first_denoise, last_denoise)
return ({"axis": axis, "values": values},) if values else (None,)
# PromptSR
class XYplot_PromptSR:
@classmethod
def INPUT_TYPES(cls):
inputs = {
"required": {
"target_prompt": (["positive", "negative"],),
"search_txt": ("STRING", {"default": "", "multiline": False}),
"replace_all_text": ("BOOLEAN", {"default": False}),
"replace_count": ("INT", {"default": 3, "min": 1, "max": 30 - 1}),
}
}
# Dynamically add replace_X inputs
for i in range(1, 30):
replace_key = f"replace_{i}"
inputs["required"][replace_key] = ("STRING", {"default": "", "multiline": False, "placeholder": replace_key})
return inputs
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, target_prompt, search_txt, replace_all_text, replace_count, **kwargs):
axis = None
if target_prompt == "positive":
axis = "advanced: Positive Prompt S/R"
elif target_prompt == "negative":
axis = "advanced: Negative Prompt S/R"
# Create base entry
values = [(search_txt, None, replace_all_text)]
if replace_count > 0:
# Append additional entries based on replace_count
values.extend([(search_txt, kwargs.get(f"replace_{i+1}"), replace_all_text) for i in range(replace_count)])
return ({"axis": axis, "values": values},) if values is not None else (None,)
# XYPlot Pos Condition
class XYplot_Positive_Cond:
@classmethod
def INPUT_TYPES(cls):
inputs = {
"optional": {
"positive_1": ("CONDITIONING",),
"positive_2": ("CONDITIONING",),
"positive_3": ("CONDITIONING",),
"positive_4": ("CONDITIONING",),
}
}
return inputs
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, positive_1=None, positive_2=None, positive_3=None, positive_4=None):
axis = "advanced: Pos Condition"
values = []
cond = []
# Create base entry
if positive_1 is not None:
values.append("0")
cond.append(positive_1)
if positive_2 is not None:
values.append("1")
cond.append(positive_2)
if positive_3 is not None:
values.append("2")
cond.append(positive_3)
if positive_4 is not None:
values.append("3")
cond.append(positive_4)
return ({"axis": axis, "values": values, "cond": cond},) if values is not None else (None,)
# XYPlot Neg Condition
class XYplot_Negative_Cond:
@classmethod
def INPUT_TYPES(cls):
inputs = {
"optional": {
"negative_1": ("CONDITIONING"),
"negative_2": ("CONDITIONING"),
"negative_3": ("CONDITIONING"),
"negative_4": ("CONDITIONING"),
}
}
return inputs
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, negative_1=None, negative_2=None, negative_3=None, negative_4=None):
axis = "advanced: Neg Condition"
values = []
cond = []
# Create base entry
if negative_1 is not None:
values.append(0)
cond.append(negative_1)
if negative_2 is not None:
values.append(1)
cond.append(negative_2)
if negative_3 is not None:
values.append(2)
cond.append(negative_3)
if negative_4 is not None:
values.append(3)
cond.append(negative_4)
return ({"axis": axis, "values": values, "cond": cond},) if values is not None else (None,)
# XYPlot Pos Condition List
class XYplot_Positive_Cond_List:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"positive": ("CONDITIONING",),
}
}
INPUT_IS_LIST = True
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, positive):
axis = "advanced: Pos Condition"
values = []
cond = []
for index, c in enumerate(positive):
values.append(str(index))
cond.append(c)
return ({"axis": axis, "values": values, "cond": cond},) if values is not None else (None,)
# XYPlot Neg Condition List
class XYplot_Negative_Cond_List:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"negative": ("CONDITIONING",),
}
}
INPUT_IS_LIST = True
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, negative):
axis = "advanced: Neg Condition"
values = []
cond = []
for index, c in enumerate(negative):
values.append(index)
cond.append(c)
return ({"axis": axis, "values": values, "cond": cond},) if values is not None else (None,)
# XY Plot: ControlNet
class XYplot_Control_Net:
parameters = ["strength", "start_percent", "end_percent"]
@classmethod
def INPUT_TYPES(cls):
def get_file_list(filenames):
return [file for file in filenames if file != "put_models_here.txt" and "lllite" not in file]
return {
"required": {
"control_net_name": (get_file_list(folder_paths.get_filename_list("controlnet")),),
"image": ("IMAGE",),
"target_parameter": (cls.parameters,),
"batch_count": ("INT", {"default": 3, "min": 1, "max": 30}),
"first_strength": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 10.0, "step": 0.01}),
"last_strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),
"first_start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
"last_start_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
"first_end_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
"last_end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
"strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}),
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
},
}
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, control_net_name, image, target_parameter, batch_count, first_strength, last_strength, first_start_percent,
last_start_percent, first_end_percent, last_end_percent, strength, start_percent, end_percent):
axis, = None,
values = []
if target_parameter == "strength":
axis = "advanced: ControlNetStrength"
values.append([(control_net_name, image, first_strength, start_percent, end_percent)])
strength_increment = (last_strength - first_strength) / (batch_count - 1) if batch_count > 1 else 0
for i in range(1, batch_count - 1):
values.append([(control_net_name, image, first_strength + i * strength_increment, start_percent,
end_percent)])
if batch_count > 1:
values.append([(control_net_name, image, last_strength, start_percent, end_percent)])
elif target_parameter == "start_percent":
axis = "advanced: ControlNetStart%"
percent_increment = (last_start_percent - first_start_percent) / (batch_count - 1) if batch_count > 1 else 0
values.append([(control_net_name, image, strength, first_start_percent, end_percent)])
for i in range(1, batch_count - 1):
values.append([(control_net_name, image, strength, first_start_percent + i * percent_increment,
end_percent)])
# Always add the last start_percent if batch_count is more than 1.
if batch_count > 1:
values.append((control_net_name, image, strength, last_start_percent, end_percent))
elif target_parameter == "end_percent":
axis = "advanced: ControlNetEnd%"
percent_increment = (last_end_percent - first_end_percent) / (batch_count - 1) if batch_count > 1 else 0
values.append([(control_net_name, image, image, strength, start_percent, first_end_percent)])
for i in range(1, batch_count - 1):
values.append([(control_net_name, image, strength, start_percent,
first_end_percent + i * percent_increment)])
if batch_count > 1:
values.append([(control_net_name, image, strength, start_percent, last_end_percent)])
return ({"axis": axis, "values": values},)
# 模型叠加
class XYplot_ModelMergeBlocks:
@classmethod
def INPUT_TYPES(s):
checkpoints = folder_paths.get_filename_list("checkpoints")
vae = ["Use Model 1", "Use Model 2"] + folder_paths.get_filename_list("vae")
preset = ["Preset"] # 20
preset += load_preset("mmb-preset.txt")
preset += load_preset("mmb-preset.custom.txt")
default_vectors = "1,0,0; \n0,1,0; \n0,0,1; \n1,1,0; \n1,0,1; \n0,1,1; "
return {
"required": {
"ckpt_name_1": (checkpoints,),
"ckpt_name_2": (checkpoints,),
"vae_use": (vae, {"default": "Use Model 1"}),
"preset": (preset, {"default": "preset"}),
"values": ("STRING", {"default": default_vectors, "multiline": True, "placeholder": 'Support 2 methods:\n\n1.input, middle, out in same line and insert values seperated by "; "\n\n2.model merge block number seperated by ", " in same line and insert values seperated by "; "'}),
},
"hidden": {"my_unique_id": "UNIQUE_ID"}
}
RETURN_TYPES = ("X_Y",)
RETURN_NAMES = ("X or Y",)
FUNCTION = "xy_value"
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, ckpt_name_1, ckpt_name_2, vae_use, preset, values, my_unique_id=None):
axis = "advanced: ModelMergeBlocks"
if ckpt_name_1 is None:
raise Exception("ckpt_name_1 is not found")
if ckpt_name_2 is None:
raise Exception("ckpt_name_2 is not found")
models = (ckpt_name_1, ckpt_name_2)
xy_values = {"axis":axis, "values":values, "models":models, "vae_use": vae_use}
return (xy_values,)
# 显示推理时间
class showSpentTime:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pipe": ("PIPE_LINE",),
"spent_time": ("INFO", {"default": '推理完成后将显示推理时间', "forceInput": False}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
FUNCTION = "notify"
OUTPUT_NODE = True
RETURN_TYPES = ()
RETURN_NAMES = ()
CATEGORY = "EasyUse/Util"
def notify(self, pipe, spent_time=None, unique_id=None, extra_pnginfo=None):
if unique_id and extra_pnginfo and "workflow" in extra_pnginfo:
workflow = extra_pnginfo["workflow"]
node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id), None)
if node:
spent_time = pipe['loader_settings']['spent_time'] if 'spent_time' in pipe['loader_settings'] else ''
node["widgets_values"] = [spent_time]
return {"ui": {"text": spent_time}, "result": {}}
# 显示加载器参数中的各种名称
class showLoaderSettingsNames:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pipe": ("PIPE_LINE",),
"names": ("INFO", {"default": '', "forceInput": False}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING",)
RETURN_NAMES = ("ckpt_name", "vae_name", "lora_name")
FUNCTION = "notify"
OUTPUT_NODE = True
CATEGORY = "EasyUse/Util"
def notify(self, pipe, names=None, unique_id=None, extra_pnginfo=None):
if unique_id and extra_pnginfo and "workflow" in extra_pnginfo:
workflow = extra_pnginfo["workflow"]
node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id), None)
if node:
ckpt_name = pipe['loader_settings']['ckpt_name'] if 'ckpt_name' in pipe['loader_settings'] else ''
vae_name = pipe['loader_settings']['vae_name'] if 'vae_name' in pipe['loader_settings'] else ''
lora_name = pipe['loader_settings']['lora_name'] if 'lora_name' in pipe['loader_settings'] else ''
if ckpt_name:
ckpt_name = os.path.basename(os.path.splitext(ckpt_name)[0])
if vae_name:
vae_name = os.path.basename(os.path.splitext(vae_name)[0])
if lora_name:
lora_name = os.path.basename(os.path.splitext(lora_name)[0])
names = "ckpt_name: " + ckpt_name + '\n' + "vae_name: " + vae_name + '\n' + "lora_name: " + lora_name
node["widgets_values"] = names
return {"ui": {"text": names}, "result": (ckpt_name, vae_name, lora_name)}
NODE_CLASS_MAPPINGS = {
# prompt 提示词
"easy positive": positivePrompt,
"easy negative": negativePrompt,
"easy wildcards": wildcardsPrompt,
"easy promptList": promptList,
"easy stylesSelector": stylesPromptSelector,
"easy portraitMaster": portraitMaster,
# loaders 加载器
"easy fullLoader": fullLoader,
"easy a1111Loader": a1111Loader,
"easy comfyLoader": comfyLoader,
"easy zero123Loader": zero123Loader,
"easy svdLoader": svdLoader,
"easy loraStack": loraStackLoader,
"easy controlnetLoader": controlnetSimple,
"easy controlnetLoaderADV": controlnetAdvanced,
# latent 潜空间
"easy latentNoisy": latentNoisy,
"easy latentCompositeMaskedWithCond": latentCompositeMaskedWithCond,
# seed 随机种
"easy seed": easySeed,
"easy globalSeed": globalSeed,
# preSampling 预采样处理
"easy preSampling": samplerSettings,
"easy preSamplingAdvanced": samplerSettingsAdvanced,
"easy preSamplingSdTurbo": sdTurboSettings,
"easy preSamplingDynamicCFG": dynamicCFGSettings,
# kSampler k采样器
"easy kSampler": samplerSimple,
"easy fullkSampler": samplerFull,
"easy kSamplerTiled": samplerSimpleTiled,
"easy kSamplerInpainting": samplerSimpleInpainting,
"easy kSamplerDownscaleUnet": samplerSimpleDownscaleUnet,
"easy kSamplerSDTurbo": samplerSDTurbo,
"easy unSampler": unsampler,
# fix 修复相关
"easy hiresFix": hiresFix,
"easy preDetailerFix": preDetailerFix,
"easy ultralyticsDetectorPipe": ultralyticsDetectorForDetailerFix,
"easy samLoaderPipe": samLoaderForDetailerFix,
"easy detailerFix": detailerFix,
# pipe 管道(节点束)
"easy pipeIn": pipeIn,
"easy pipeOut": pipeOut,
"easy pipeToBasicPipe": pipeToBasicPipe,
"easy XYPlot": pipeXYPlot,
"easy XYPlotAdvanced": pipeXYPlotAdvanced,
# XY Inputs
"easy XYInputs: Seeds++ Batch": XYplot_SeedsBatch,
"easy XYInputs: Steps": XYplot_Steps,
"easy XYInputs: CFG Scale": XYplot_CFG,
"easy XYInputs: Sampler/Scheduler": XYplot_Sampler_Scheduler,
"easy XYInputs: Denoise": XYplot_Denoise,
"easy XYInputs: ModelMergeBlocks": XYplot_ModelMergeBlocks,
"easy XYInputs: PromptSR": XYplot_PromptSR,
"easy XYInputs: ControlNet": XYplot_Control_Net,
"easy XYInputs: PositiveCond": XYplot_Positive_Cond,
"easy XYInputs: PositiveCondList": XYplot_Positive_Cond_List,
"easy XYInputs: NegativeCond": XYplot_Negative_Cond,
"easy XYInputs: NegativeCondList": XYplot_Negative_Cond_List,
# others 其他
"easy showSpentTime": showSpentTime,
"easy showLoaderSettingsNames": showLoaderSettingsNames,
# "easy imageRemoveBG": imageREMBG,
"dynamicThresholdingFull": dynamicThresholdingFull,
# __for_testing 测试
"easy fooocusInpaintLoader": fooocusInpaintLoader
}
NODE_DISPLAY_NAME_MAPPINGS = {
# prompt 提示词
"easy positive": "Positive",
"easy negative": "Negative",
"easy wildcards": "Wildcards",
"easy promptList": "PromptList",
"easy stylesSelector": "Styles Selector",
"easy portraitMaster": "Portrait Master",
# loaders 加载器
"easy fullLoader": "EasyLoader (Full)",
"easy a1111Loader": "EasyLoader (A1111)",
"easy comfyLoader": "EasyLoader (Comfy)",
"easy zero123Loader": "EasyLoader (Zero123)",
"easy svdLoader": "EasyLoader (SVD)",
"easy loraStack": "EasyLoraStack",
"easy controlnetLoader": "EasyControlnet",
"easy controlnetLoaderADV": "EasyControlnet (Advanced)",
"easy photoMakerApply": "Apply PhotoMaker",
# latent 潜空间
"easy latentNoisy": "LatentNoisy",
"easy latentCompositeMaskedWithCond": "LatentCompositeMaskedWithCond",
# seed 随机种
"easy seed": "EasySeed",
"easy globalSeed": "EasyGlobalSeed",
# preSampling 预采样处理
"easy preSampling": "PreSampling",
"easy preSamplingAdvanced": "PreSampling (Advanced)",
"easy preSamplingSdTurbo": "PreSampling (SDTurbo)",
"easy preSamplingDynamicCFG": "PreSampling (DynamicCFG)",
# kSampler k采样器
"easy kSampler": "EasyKSampler",
"easy fullkSampler": "EasyKSampler (Full)",
"easy kSamplerTiled": "EasyKSampler (Tiled Decode)",
"easy kSamplerInpainting": "EasyKSampler (Inpainting)",
"easy kSamplerDownscaleUnet": "EasyKsampler (Downscale Unet)",
"easy kSamplerSDTurbo": "EasyKSampler (SDTurbo)",
"easy unSampler": "EasyUnSampler",
# fix 修复相关
"easy hiresFix": "HiresFix",
"easy preDetailerFix": "PreDetailerFix",
"easy ultralyticsDetectorPipe": "UltralyticsDetector (Pipe)",
"easy samLoaderPipe": "SAMLoader (Pipe)",
"easy detailerFix": "DetailerFix",
# pipe 管道(节点束)
"easy pipeIn": "Pipe In",
"easy pipeOut": "Pipe Out",
"easy pipeToBasicPipe": "Pipe -> BasicPipe",
"easy XYPlot": "XY Plot",
"easy XYPlotAdvanced": "XY Plot Advanced",
# XY Inputs
"easy XYInputs: Seeds++ Batch": "XY Inputs: Seeds++ Batch //EasyUse",
"easy XYInputs: Steps": "XY Inputs: Steps //EasyUse",
"easy XYInputs: CFG Scale": "XY Inputs: CFG Scale //EasyUse",
"easy XYInputs: Sampler/Scheduler": "XY Inputs: Sampler/Scheduler //EasyUse",
"easy XYInputs: Denoise": "XY Inputs: Denoise //EasyUse",
"easy XYInputs: ModelMergeBlocks": "XY Inputs: ModelMergeBlocks //EasyUse",
"easy XYInputs: PromptSR": "XY Inputs: PromptSR //EasyUse",
"easy XYInputs: ControlNet": "XY Inputs: Controlnet //EasyUse",
"easy XYInputs: PositiveCond": "XY Inputs: PosCond //EasyUse",
"easy XYInputs: PositiveCondList": "XY Inputs: PosCondList //EasyUse",
"easy XYInputs: NegativeCond": "XY Inputs: NegCond //EasyUse",
"easy XYInputs: NegativeCondList": "XY Inputs: NegCondList //EasyUse",
# others 其他
"easy showSpentTime": "Show Spent Time",
"easy showLoaderSettingsNames": "Show Loader Settings Names",
"easy imageRemoveBG": "ImageRemoveBG",
"dynamicThresholdingFull": "DynamicThresholdingFull",
# __for_testing 测试
"easy fooocusInpaintLoader": "Load Fooocus Inpaint"
}