Files
hanzo-easyuse/py/easyNodes.py
T
2023-12-10 15:07:55 +08:00

1793 lines
72 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 torch
import psutil
import random
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
from pathlib import Path
import comfy.model_management
from comfy.sd import CLIP, VAE
from comfy.cli_args import args
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 nodes import MAX_RESOLUTION, VAEEncode, VAEEncodeTiled, VAEDecode, VAEDecodeTiled
from .config import BASE_RESOLUTIONS
from server import PromptServer
class CC:
CLEAN = '\33[0m'
BOLD = '\33[1m'
ITALIC = '\33[3m'
UNDERLINE = '\33[4m'
BLINK = '\33[5m'
BLINK2 = '\33[6m'
SELECTED = '\33[7m'
BLACK = '\33[30m'
RED = '\33[31m'
GREEN = '\33[32m'
YELLOW = '\33[33m'
BLUE = '\33[34m'
VIOLET = '\33[35m'
BEIGE = '\33[36m'
WHITE = '\33[37m'
GREY = '\33[90m'
LIGHTRED = '\33[91m'
LIGHTGREEN = '\33[92m'
LIGHTYELLOW = '\33[93m'
LIGHTBLUE = '\33[94m'
LIGHTVIOLET = '\33[95m'
LIGHTBEIGE = '\33[96m'
LIGHTWHITE = '\33[97m'
class easyL:
def __init__(self, input_string):
self.header_value = f'{CC.LIGHTGREEN}[easy] {CC.GREEN}'
self.label_value = ''
self.title_value = ''
self.input_string = f'{input_string}{CC.CLEAN}'
def h(self, header_value):
self.header_value = f'{CC.LIGHTGREEN}[{header_value}] {CC.GREEN}'
return self
def full(self):
self.h('easyNodes')
return self
def success(self):
self.label_value = f'Success: '
return self
def warn(self):
self.label_value = f'{CC.RED}Warning:{CC.LIGHTRED} '
return self
def error(self):
self.label_value = f'{CC.LIGHTRED}ERROR:{CC.RED} '
return self
def t(self, title_value):
self.title_value = f'{title_value}:{CC.CLEAN} '
return self
def p(self):
print(self.header_value + self.label_value + self.title_value + self.input_string)
return self
def interrupt(self, msg):
raise Exception(msg)
class easypaths:
ComfyUI = folder_paths.base_path
easyNodes = Path(__file__).parent
# 加载
class easyLoader:
def __init__(self):
self.loaded_objects = {
"ckpt": defaultdict(tuple), # {ckpt_name: (model, ...)}
"clip": 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"))
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):
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"]:
return self.loaded_objects["ckpt"][cache_name][0], self.loaded_objects["clip"][cache_name][0], \
self.loaded_objects["bvae"][cache_name][0]
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
print(config_name)
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=True,
embedding_directory=folder_paths.get_folder_paths("embeddings"))
else:
loaded_ckpt = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True,
embedding_directory=folder_paths.get_folder_paths(
"embeddings"))
self.add_to_cache("ckpt", cache_name, loaded_ckpt[0])
self.add_to_cache("clip", cache_name, loaded_ckpt[1])
self.add_to_cache("bvae", cache_name, loaded_ckpt[2])
self.eviction_based_on_memory()
return loaded_ckpt[0], loaded_ckpt[1], 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, spent_time: str) -> Tuple:
"""Return a tuple of various elements fetched from the input pipe dictionary."""
return (
pipe,
pipe.get("images"),
spent_time
)
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")
)
easyCache = easyLoader()
sampler = easySampler()
def nsp_parse(text, seed=0, noodle_key='__', nspterminology=None, pantry_path=None, title=None, my_unique_id=None):
if "__" not in text:
return text
if nspterminology is None:
# Fetch the NSP Pantry
if pantry_path is None:
pantry_path = os.path.join(easypaths.easyNodes, 'nsp_pantry.json')
if not os.path.exists(pantry_path):
response = urlopen('https://raw.githubusercontent.com/WASasquatch/noodle-soup-prompts/main/nsp_pantry.json')
tmp_pantry = json.loads(response.read())
# Dump JSON locally
pantry_serialized = json.dumps(tmp_pantry, indent=4)
with open(pantry_path, "w") as f:
f.write(pantry_serialized)
del response, tmp_pantry
# Load local pantry
with open(pantry_path, 'r') as f:
nspterminology = json.load(f)
if seed > 0 or seed < 0:
random.seed(seed)
# Parse Text
new_text = text
for term in nspterminology:
# Target Noodle
tkey = f'{noodle_key}{term}{noodle_key}'
# How many occurrences?
tcount = new_text.count(tkey)
if tcount > 0:
nsp_parsed = True
# Apply random results for each noodle counted
for _ in range(tcount):
new_text = new_text.replace(
tkey, random.choice(nspterminology[term]), 1)
seed += 1
random.seed(seed)
easyL(new_text).t(f'{title}[{my_unique_id}]').p()
return new_text
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."""
easyL(f"Folder {folder} does not exist. Attempting to create...").warn().p()
if not os.path.exists(folder):
try:
os.makedirs(folder)
easyL(f"{folder} Created Successfully").success().p()
except OSError:
easyL(f"Failed to create folder {folder}").error().p()
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
# if "text" in collected_inputs:
# del collected_inputs['text']
# print(collected_inputs)
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 filename_parser(output_dir: str, filename_prefix: str, prompt: Dict[str, dict], my_unique_id: str,
number_padding: int, group_id: int, ext: str) -> str:
"""Parse the filename using provided patterns and replace them with actual values."""
subfolder = os.path.dirname(os.path.normpath(filename_prefix))
filename = os.path.basename(os.path.normpath(filename_prefix))
filename = re.sub(r'%date:(.*?)%', lambda m: easySave._format_date(m.group(1), datetime.datetime.now()),
filename_prefix)
all_inputs = easySave._gather_all_inputs(prompt, my_unique_id)
filename = re.sub(r'%(.*?)%', lambda m: str(all_inputs.get(m.group(1), '')), filename)
filename = re.sub(r'[/\\]+', '-', filename)
filename = easySave._get_filename_with_padding(output_dir, filename, number_padding, group_id, ext)
return filename, subfolder
@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", "Sender/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 in ("Preview", "Sender"):
output_dir = self.output_dir
filename_prefix = 'easyPreview'
results = list()
for image in images:
img = Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8))
filename = filename_prefix.replace("%width%", str(img.size[0])).replace("%height%", str(img.size[1]))
filename, subfolder = easySave.filename_parser(output_dir, filename, self.prompt, self.my_unique_id,
self.number_padding, group_id, ext)
file_path = os.path.join(output_dir, filename)
if ext == "png" and 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))
if self.overwrite_existing or not os.path.isfile(file_path):
img.save(file_path, pnginfo=metadata, format=FORMAT_MAP[ext])
else:
if self.overwrite_existing or not os.path.isfile(file_path):
img.save(file_path, format=FORMAT_MAP[ext])
else:
easyL(f"File {file_path} already exists... Skipping").error().p()
results.append({
"filename": file_path,
"subfolder": subfolder,
"type": self.type
})
return results
def textfile(self, text, filename_prefix, output_type, group_id=0, ext='txt'):
if output_type == "Hide":
return []
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()
if output_type == "Preview":
filename_prefix = 'easyPreview'
filename = easySave.filename_parser(output_dir, filename_prefix, self.prompt, self.my_unique_id,
self.number_padding, group_id, ext)
file_path = os.path.join(output_dir, filename)
if self.overwrite_existing or not os.path.isfile(file_path):
with open(file_path, 'w') as f:
f.write(text)
else:
easyL(f"File {file_path} already exists... Skipping").error().p()
#---------------------------------------------------------------加载器 开始----------------------------------------------------------------------#
# A1111简易加载器
class a1111Loader:
@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,),
"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}),
},
"optional": {"optional_lora_stack": ("LORA_STACK",)},
"hidden": {"prompt": "PROMPT", "positive_weight_interpretation": "A1111", "negative_weight_interpretation": "A1111"}, "my_unique_id": "UNIQUE_ID"}
RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE")
RETURN_NAMES = ("pipe", "model", "VAE")
FUNCTION = "adv_pipeloader"
CATEGORY = "EasyUse/Loader"
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, optional_lora_stack=None, prompt=None,
positive_weight_interpretation='A1111', negative_weight_interpretation='A1111',
my_unique_id=None
):
model: ModelPatcher | None = None
clip: CLIP | None = None
vae: VAE | None = 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.")
# Create Empty Latent
latent = torch.zeros([1, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu()
samples = {"samples": latent}
# Clean models from loaded_objects
easyCache.update_loaded_objects(prompt)
# Load models
model, clip, vae = easyCache.load_checkpoint(ckpt_name, "Default")
if optional_lora_stack is not None:
for lora in optional_lora_stack:
model, clip = easyCache.load_lora(lora[0], model, clip, lora[1], lora[2])
if lora_name != "None":
model, clip = easyCache.load_lora(lora_name, model, clip, lora_model_strength, lora_clip_strength)
# CLIP skip
if not clip:
raise Exception("No CLIP found")
clipped = clip.clone()
if clip_skip != 0:
clipped.clip_layer(clip_skip)
positive = nsp_parse(positive, 0, title='pipeLoader Positive', my_unique_id=my_unique_id)
positive_embeddings_final, positive_pooled = advanced_encode(clipped, positive, "none",
positive_weight_interpretation, w_max=1.0,
apply_to_pooled='enable')
positive_embeddings_final = [[positive_embeddings_final, {"pooled_output": positive_pooled}]]
negative = nsp_parse(negative, 0, title='pipeLoader Negative', my_unique_id=my_unique_id)
negative_embeddings_final, negative_pooled = advanced_encode(clipped, negative, "none",
negative_weight_interpretation, w_max=1.0,
apply_to_pooled='enable')
negative_embeddings_final = [[negative_embeddings_final, {"pooled_output": negative_pooled}]]
image = easySampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0)))
pipe = {"model": model,
"positive": positive_embeddings_final,
"negative": negative_embeddings_final,
"vae": vae,
"clip": clip,
"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,
"refiner_ckpt_name": None,
"refiner_vae_name": None,
"refiner_lora1_name": None,
"refiner_lora1_model_strength": None,
"refiner_lora1_clip_strength": None,
"refiner_lora2_name": None,
"refiner_lora2_model_strength": None,
"refiner_lora2_clip_strength": None,
"clip_skip": clip_skip,
"positive": positive,
"positive_l": None,
"positive_g": None,
"positive_token_normalization": "none",
"positive_weight_interpretation": positive_weight_interpretation,
"positive_balance": None,
"negative": negative,
"negative_l": None,
"negative_g": None,
"negative_token_normalization": "none",
"negative_weight_interpretation": negative_weight_interpretation,
"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)
# 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,),
"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}),
},
"optional": {"optional_lora_stack": ("LORA_STACK",)},
"hidden": {"prompt": "PROMPT", "positive_weight_interpretation": "comfy", "negative_weight_interpretation": "comfy"}, "my_unique_id": "UNIQUE_ID"}
RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE")
RETURN_NAMES = ("pipe", "model", "vae")
FUNCTION = "adv_pipeloader"
CATEGORY = "EasyUse/Loader"
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, optional_lora_stack=None, prompt=None,
positive_weight_interpretation='comfy', negative_weight_interpretation='comfy',
my_unique_id=None
):
return a1111Loader.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, optional_lora_stack, prompt,
positive_weight_interpretation, negative_weight_interpretation,
my_unique_id
)
#---------------------------------------------------------------预采样 开始----------------------------------------------------------------------#
# controlnet
class controlnetSimple:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pipe": ("PIPE_LINE",),
"control_net_name": (folder_paths.get_filename_list("controlnet"),),
"image": ("IMAGE",),
},
"optional": {
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01})
}
}
RETURN_TYPES = ("PIPE_LINE",)
RETURN_NAMES = ("pipe",)
OUTPUT_NODE = True
FUNCTION = "controlnetApply"
CATEGORY = "EasyUse/PreSampling"
def controlnetApply(self, pipe, control_net_name, image, positive=None, negative=None, strength=1):
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"] if positive is None else positive
_negative = pipe["negative"] if negative is None else 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]
# 拼接条件
positive = _positive if positive is None else _positive + pipe['positive']
negative = _negative if negative is None else _negative + pipe['negative']
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,)
# 全局Seed
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/PreSampling"
OUTPUT_NODE = True
def doit(self, **kwargs):
return {}
def control_seed(action, value):
if action == 'increment':
value += 1
if value > 1125899906842624:
value = 0
elif action == 'decrement':
value -= 1
if value < 0:
value = 1125899906842624
elif action == 'randomize':
value = random.randint(0, 1125899906842624)
return value
# 预采样设置(基础)
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}),
"control_before_generate": (["fixed", "increment", "decrement", "randomize"], {"default": "randomize"}),
},
"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, control_before_generate, prompt=None, extra_pnginfo=None, my_unique_id=None):
# seed生成
seed_num = control_seed(control_before_generate, seed_num)
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:
length = len(node["widgets_values"])
node["widgets_values"][length-2] = seed_num
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"],
"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}),
"control_before_generate": (["fixed", "increment", "decrement", "randomize"], {"default": "randomize"}),
},
"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, control_before_generate, prompt=None, extra_pnginfo=None, my_unique_id=None):
# seed生成
seed_num = control_seed(control_before_generate, seed_num)
if my_unique_id and add_noise == 'enabled':
workflow = extra_pnginfo["workflow"]
node = next((x for x in workflow["nodes"] if str(x["id"]) == my_unique_id), None)
if node:
length = len(node["widgets_values"])
node["widgets_values"][length-2] = seed_num
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"],
"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}),
"control_before_generate": (["fixed", "increment", "decrement", "randomize"], {"default": "randomize"}),
},
"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, control_before_generate, 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
# seed生成
seed_num = control_seed(control_before_generate, seed_num)
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:
length = len(node["widgets_values"])
node["widgets_values"][length-2] = seed_num
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}),
"control_before_generate": (["fixed", "increment", "decrement", "randomize"], {"default": "randomize"}),
},
"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, control_before_generate, 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)
# seed生成
seed_num = control_seed(control_before_generate, seed_num)
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:
length = len(node["widgets_values"])
node["widgets_values"][length-2] = seed_num
new_pipe = {
"model": m,
"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"],
"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 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"],),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
"optional": {
"model": ("MODEL",),
# "text": ("INFO", {"default": '推理完成后将显示推理时间', "multiline": False, "forceInput": False}),
},
"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):
# 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']
steps = 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 = pipe['loader_settings']['cfg']
sampler_name = pipe['loader_settings']['sampler_name']
scheduler = pipe['loader_settings']['scheduler']
denoise = 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 vae_decode_latent(vae, samples, tile_size):
return VAEDecodeTiled().decode(vae, samples, tile_size)[0] if tile_size is not None else VAEDecode().decode(vae, samples)[0]
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):
# clean spent time in prompt
# 推理初始时间
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)
# 解码图片
samp_images = vae_decode_latent(samp_vae, samp_samples, tile_size)
# 推理总耗时(包含解码)
end_decode_time = int(time.time() * 1000)
text = 'Diffusion:' + str((end_time-start_time)/1000)+'s, VAEDecode:' + str((end_decode_time-end_time)/1000)+'s'
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"],
}
sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe)
del pipe
if image_output in ("Hide", "Hide/Save"):
return {"ui": {"text": text},
"result": sampler.get_output(new_pipe, text)}
if image_output in ("Sender", "Sender/Save"):
PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results})
return {"ui": {"text": text, "images": results},
"result": sampler.get_output(new_pipe, text)}
preview_latent = True
if image_output in ("Hide", "Hide/Save"):
preview_latent = False
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)
# 简易采样器 (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"],),
"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 samplerSimple.run(self, pipe, image_output, link_id, save_prefix, model, 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"],),
"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:
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 = 'Diffusion:' + str((end_time - start_time) / 1000) + 's, Decode:' + str(
(end_decode_time - end_time) / 1000) + 's'
# 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"],
}
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, spent_time)}
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, spent_time)}
# showSpentTime
# class showSpentTime:
# @classmethod
# def INPUT_TYPES(s):
# return {
# "required": {
# "spent_time": ("STRING", {"forceInput": True}),
# },
# "hidden": {
# "unique_id": "UNIQUE_ID",
# "extra_pnginfo": "EXTRA_PNGINFO",
# },
# }
#
# INPUT_IS_LIST = True
# FUNCTION = "notify"
# OUTPUT_NODE = True
# RETURN_TYPES = ()
# RETURN_NAMES = ()
#
# CATEGORY = "utils"
#
# def notify(self, spent_time, unique_id=None, extra_pnginfo=None):
# if unique_id and extra_pnginfo and "workflow" in extra_pnginfo[0]:
# workflow = extra_pnginfo[0]["workflow"]
# node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id[0]), None)
# if node:
# node["widgets_values"] = [spent_time]
# return {"ui": {"text": spent_time}, "result": {}}
NODE_CLASS_MAPPINGS = {
"easy a1111Loader": a1111Loader,
"easy comfyLoader": comfyLoader,
"easy controlnetLoader": controlnetSimple,
"easy globalSeed": globalSeed,
"easy preSampling": samplerSettings,
"easy preSamplingAdvanced": samplerSettingsAdvanced,
"easy preSamplingSdTurbo": sdTurboSettings,
"easy preSamplingDynamicCFG": dynamicCFGSettings,
"easy kSampler": samplerSimple,
"easy kSamplerTiled": samplerSimpleTiled,
"easy kSamplerSDTurbo": samplerSDTurbo,
"dynamicThresholdingFull": dynamicThresholdingFull
# "showSpentTime": showSpentTime
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy a1111Loader": "EasyLoader (A1111)",
"easy comfyLoader": "EasyLoader (comfy)",
"easy controlnetLoader": "EasyControlnet",
"easy globalSeed": "GlobalSeed",
"easy preSampling": "PreSampling",
"easy preSamplingAdvanced": "PreSampling (Advanced)",
"easy preSamplingSdTurbo": "PreSampling (SDTurbo)",
"easy preSamplingDynamicCFG": "PreSampling (DynamicCFG)",
"easy kSampler": "EasyKSampler",
"easy kSamplerTiled": "EasyKSampler (Tiled Decode)",
"easy kSamplerSDTurbo": "EasyKSampler (SDTurbo)",
"dynamicThresholdingFull": "DynamicThresholdingFull"
# "easy showSpentTime": "ShowSpentTime"
}