Add files via upload

**Efficiency Nodes v1.35 Changes**

Bug fixes:
- The Efficient Loader no longer endlessly loads checkpoints/VAEs into RAM.
- Fixed an issue with the XY Plot calling for a potentially non-existant font.
- Fixed a bug where the XY Plot image results were being cropped at bottom.

Changes:
- The KSampler Efficient's "my_unique_id" has been removed.
- Script results now preview regardless of the "preview_image" setting.
- Efficient Loader node now supports LoRA.
- Added 'Scheduler' as an option type for the XY Plot node.
This commit is contained in:
TSC
2023-04-22 13:51:45 -05:00
committed by GitHub
parent 49ba99069d
commit 0b37a61109
2 changed files with 263 additions and 59 deletions

BIN
arial.ttf Normal file

Binary file not shown.

View File

@@ -9,6 +9,7 @@ from PIL.PngImagePlugin import PngInfo
import numpy as np import numpy as np
import torch import torch
from pathlib import Path
import os import os
import sys import sys
import json import json
@@ -23,6 +24,9 @@ comfy_dir = os.path.abspath(os.path.join(my_dir, '..', '..'))
# Add the ComfyUI directory path to the sys.path list # Add the ComfyUI directory path to the sys.path list
sys.path.append(comfy_dir) sys.path.append(comfy_dir)
# Construct the path to the font file
font_path = os.path.join(my_dir, 'arial.ttf')
# Import functions from nodes.py in the ComfyUI directory # Import functions from nodes.py in the ComfyUI directory
import comfy.samplers import comfy.samplers
import comfy.sd import comfy.sd
@@ -38,16 +42,68 @@ def tensor2pil(image: torch.Tensor) -> Image.Image:
def pil2tensor(image: Image.Image) -> torch.Tensor: def pil2tensor(image: Image.Image) -> torch.Tensor:
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def resolve_input_links(prompt, input_value):
if isinstance(input_value, list) and len(input_value) == 2:
input_id, input_index = input_value
return prompt[input_id]['inputs'][list(prompt[input_id]['inputs'].keys())[input_index]]
return input_value
# TSC Efficient Loader # Cache models in RAM
# Track what objects have already been loaded into memory
loaded_objects = { loaded_objects = {
"ckpt": [], # (ckpt_name, location) "ckpt": [], # (ckpt_name, location)
"clip": [], # (ckpt_name, location) "clip": [], # (ckpt_name, location)
"bvae": [], # (ckpt_name, location) "bvae": [], # (ckpt_name, location)
"vae": [] # (vae_name, location) "vae": [], # (vae_name, location)
"lora": [] # (lora_name, location)
} }
def print_loaded_objects_entries():
print("\n" + "-" * 40) # Print an empty line followed by a separator line
for key in ["ckpt", "clip", "bvae", "vae", "lora"]:
print(f"{key.capitalize()} entries:")
for entry in loaded_objects[key]:
truncated_name = entry[0][:20]
print(f" Name: {truncated_name}\n Location: {entry[1]}")
if len(entry) == 3:
print(f" Entry[2]: {entry[2]}")
print("-" * 40) # Print a separator line
print("\n") # Print an empty line
def update_loaded_objects(prompt):
global loaded_objects
# Extract all Efficient Loader class type entries
efficient_loader_entries = [entry for entry in prompt.values() if entry["class_type"] == "Efficient Loader"]
# Collect all desired model, vae, and lora names
desired_ckpt_names = set()
desired_vae_names = set()
desired_lora_names = set()
for entry in efficient_loader_entries:
desired_ckpt_names.add(entry["inputs"]["ckpt_name"])
desired_vae_names.add(entry["inputs"]["vae_name"])
desired_lora_names.add(entry["inputs"]["lora_name"])
# Check and clear unused ckpt, clip, and bvae entries
for list_key in ["ckpt", "clip", "bvae"]:
unused_indices = [i for i, entry in enumerate(loaded_objects[list_key]) if entry[0] not in desired_ckpt_names]
for index in sorted(unused_indices, reverse=True):
loaded_objects[list_key].pop(index)
# Check and clear unused vae entries
unused_vae_indices = [i for i, entry in enumerate(loaded_objects["vae"]) if entry[0] not in desired_vae_names]
for index in sorted(unused_vae_indices, reverse=True):
loaded_objects["vae"].pop(index)
# Check and clear unused lora entries
unused_lora_indices = [i for i, entry in enumerate(loaded_objects["lora"]) if entry[0] not in desired_lora_names]
for index in sorted(unused_lora_indices, reverse=True):
loaded_objects["lora"].pop(index)
def load_checkpoint(ckpt_name,output_vae=True, output_clip=True): def load_checkpoint(ckpt_name,output_vae=True, output_clip=True):
""" """
Searches for tuple index that contains ckpt_name in "ckpt" array of loaded_objects. Searches for tuple index that contains ckpt_name in "ckpt" array of loaded_objects.
@@ -85,6 +141,7 @@ def load_checkpoint(ckpt_name,output_vae=True, output_clip=True):
return model, clip, vae return model, clip, vae
def load_vae(vae_name): def load_vae(vae_name):
""" """
Extracts the vae with a given name from the "vae" array in loaded_objects. Extracts the vae with a given name from the "vae" array in loaded_objects.
@@ -103,6 +160,38 @@ def load_vae(vae_name):
loaded_objects["vae"].append((vae_name, vae)) loaded_objects["vae"].append((vae_name, vae))
return vae return vae
def load_lora(lora_name, model, clip, strength_model, strength_clip):
"""
Extracts the Lora model with a given name from the "lora" array in loaded_objects.
If the Lora model is not found or the strength values change, creates a new Lora object with the given name and adds it to the "lora" array.
"""
global loaded_objects
# Check if lora_name exists in "lora" array
existing_lora = [entry for entry in loaded_objects["lora"] if entry[0] == lora_name]
if existing_lora:
lora_name, model_lora, clip_lora, stored_strength_model, stored_strength_clip = existing_lora[0]
if strength_model == stored_strength_model and strength_clip == stored_strength_clip:
return model_lora, clip_lora
# If Lora model not found or strength values changed, generate new Lora models
lora_path = folder_paths.get_full_path("loras", lora_name)
model_lora, clip_lora = comfy.sd.load_lora_for_models(model, clip, lora_path, strength_model, strength_clip)
# Remove existing Lora model if it exists
if existing_lora:
loaded_objects["lora"].remove(existing_lora[0])
# Update loaded_objects[] array
loaded_objects["lora"].append((lora_name, model_lora, clip_lora, strength_model, strength_clip))
return model_lora, clip_lora
# TSC Efficient Loader
class TSC_EfficientLoader: class TSC_EfficientLoader:
@classmethod @classmethod
@@ -110,21 +199,23 @@ class TSC_EfficientLoader:
return {"required": { "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ), return {"required": { "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),
"vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),), "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
"clip_skip": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}), "clip_skip": ("INT", {"default": -1, "min": -24, "max": -1, "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}),
"positive": ("STRING", {"default": "Positive","multiline": True}), "positive": ("STRING", {"default": "Positive","multiline": True}),
"negative": ("STRING", {"default": "Negative", "multiline": True}), "negative": ("STRING", {"default": "Negative", "multiline": True}),
"empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}), "empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
"empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}), "empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}) "batch_size": ("INT", {"default": 1, "min": 1, "max": 64})},
}} "hidden": {"prompt": "PROMPT"}}
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP" ,) RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP" ,)
RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "CLIP", ) RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "CLIP", )
FUNCTION = "efficientloader" FUNCTION = "efficientloader"
CATEGORY = "Efficiency Nodes/Loaders" CATEGORY = "Efficiency Nodes/Loaders"
def efficientloader(self, ckpt_name, vae_name, clip_skip, positive, negative, empty_latent_width, empty_latent_height, batch_size, def efficientloader(self, ckpt_name, vae_name, clip_skip, lora_name, lora_model_strength, lora_clip_strength,
output_vae=False, output_clip=True): positive, negative, empty_latent_width, empty_latent_height, batch_size, prompt=None):
model: ModelPatcher | None = None model: ModelPatcher | None = None
clip: CLIP | None = None clip: CLIP | None = None
@@ -133,16 +224,21 @@ class TSC_EfficientLoader:
# Create Empty Latent # Create Empty Latent
latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu() latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu()
# Check for "Baked VAE" selected # Clean models from loaded_objects
if vae_name == "Baked VAE": update_loaded_objects(prompt)
output_vae = True
model, clip, vae = load_checkpoint(ckpt_name,output_vae) # Load models
model, clip, vae = load_checkpoint(ckpt_name)
if lora_name != "None":
model, clip = load_lora(lora_name, model, clip, lora_model_strength, lora_clip_strength)
# Check for custom VAE # Check for custom VAE
if vae_name != "Baked VAE": if vae_name != "Baked VAE":
vae = load_vae(vae_name) vae = load_vae(vae_name)
#print_loaded_objects_entries()
# CLIP skip # CLIP skip
if not clip: if not clip:
raise Exception("No CLIP found") raise Exception("No CLIP found")
@@ -151,12 +247,63 @@ class TSC_EfficientLoader:
return (model, [[clip.encode(positive), {}]], [[clip.encode(negative), {}]], {"samples":latent}, vae, clip, ) return (model, [[clip.encode(positive), {}]], [[clip.encode(negative), {}]], {"samples":latent}, vae, clip, )
# KSampler Efficient ID finder
last_returned_ids = {}
def find_k_sampler_id(prompt, sampler_state=None, seed=None, steps=None, cfg=None,
sampler_name=None, scheduler=None, denoise=None, preview_image=None):
global last_returned_ids
input_params = [
('sampler_state', sampler_state),
('seed', seed),
('steps', steps),
('cfg', cfg),
('sampler_name', sampler_name),
('scheduler', scheduler),
('denoise', denoise),
('preview_image', preview_image),
]
matching_ids = []
for key, value in prompt.items():
if value.get('class_type') == 'KSampler (Efficient)':
inputs = value['inputs']
match = all(inputs[param_name] == param_value for param_name, param_value in input_params if param_value is not None)
if match:
matching_ids.append(key)
if matching_ids:
input_key = tuple(param_value for param_name, param_value in input_params)
if input_key in last_returned_ids:
last_id = last_returned_ids[input_key]
next_id = None
for id in matching_ids:
if id > last_id:
if next_id is None or id < next_id:
next_id = id
if next_id is None:
# All IDs have been used; start again from the first one
next_id = min(matching_ids)
else:
next_id = min(matching_ids)
last_returned_ids[input_key] = next_id
return next_id
else:
last_returned_ids.clear()
return None
# TSC KSampler (Efficient) # TSC KSampler (Efficient)
last_helds: dict[str, list] = { last_helds: dict[str, list] = {
"results": [None for _ in range(15)], "results": [],
"latent": [None for _ in range(15)], "latent": [],
"images": [None for _ in range(15)], "images": [],
"vae_decode": [False for _ in range(15)] "vae_decode": []
} }
class TSC_KSampler: class TSC_KSampler:
@@ -170,7 +317,6 @@ class TSC_KSampler:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return {"required": return {"required":
{"sampler_state": (["Sample", "Hold", "Script"], ), {"sampler_state": (["Sample", "Hold", "Script"], ),
"my_unique_id": ("INT", {"default": 0, "min": 0, "max": 15}),
"model": ("MODEL",), "model": ("MODEL",),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}), "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
@@ -190,11 +336,17 @@ class TSC_KSampler:
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "IMAGE", ) RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "IMAGE", )
RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "IMAGE", ) RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "IMAGE", )
FUNCTION = "sample"
OUTPUT_NODE = True OUTPUT_NODE = True
FUNCTION = "sample"
CATEGORY = "Efficiency Nodes/Sampling" CATEGORY = "Efficiency Nodes/Sampling"
def sample(self, sampler_state, my_unique_id, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, def execute(self, sampler_state, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
denoise, preview_image, optional_vae=None, script=None):
# TODO: Implement the logic to sample from the model using the given input parameters
# Return the outputs as a tuple with the same order and types as defined in RETURN_TYPES and RETURN_NAMES
return model, positive, negative, latent_image, optional_vae, self.empty_image
def sample(self, sampler_state, model, seed, steps, cfg, sampler_name, scheduler, positive, negative,
latent_image, preview_image, denoise=1.0, prompt=None, extra_pnginfo=None, optional_vae=(None,), script=None): latent_image, preview_image, denoise=1.0, prompt=None, extra_pnginfo=None, optional_vae=(None,), script=None):
# Functions for previewing images in Ksampler # Functions for previewing images in Ksampler
@@ -252,30 +404,50 @@ class TSC_KSampler:
counter += 1 counter += 1
return results return results
def get_value_by_id(key: str, my_unique_id):
global last_helds
for value, id_ in last_helds[key]:
if id_ == my_unique_id:
return value
return None
def update_value_by_id(key: str, my_unique_id, new_value):
global last_helds
for i, (value, id_) in enumerate(last_helds[key]):
if id_ == my_unique_id:
last_helds[key][i] = (new_value, id_)
return True
last_helds[key].append((new_value, my_unique_id))
return True
my_unique_id = int(find_k_sampler_id(prompt, sampler_state, seed, steps, cfg, sampler_name,scheduler, denoise, preview_image))
# Vae input check # Vae input check
vae = optional_vae vae = optional_vae
if vae == (None,): if vae == (None,):
print('\033[32mKSampler(Efficient)[{}] Warning:\033[0m No vae input detected, preview image disabled'.format(my_unique_id)) print('\033[32mKSampler(Efficient)[{}] Warning:\033[0m No vae input detected, preview and output image disabled.\n'.format(my_unique_id))
preview_image = "Disabled" preview_image = "Disabled"
# Init last_results # Init last_results
if last_helds["results"][my_unique_id] == None: if get_value_by_id("results", my_unique_id) is None:
last_results = list() last_results = list()
else: else:
last_results = last_helds["results"][my_unique_id] last_results = get_value_by_id("results", my_unique_id)
# Init last_latent # Init last_latent
if last_helds["latent"][my_unique_id] == None: if get_value_by_id("latent", my_unique_id) is None:
last_latent = latent_image last_latent = latent_image
else: else:
last_latent = {"samples": None} last_latent = {"samples": None}
last_latent["samples"] = last_helds["latent"][my_unique_id] last_latent["samples"] = get_value_by_id("latent", my_unique_id)
# Init last_images # Init last_images
if last_helds["images"][my_unique_id] == None: if get_value_by_id("images", my_unique_id) == None:
last_images = TSC_KSampler.empty_image last_images = TSC_KSampler.empty_image
else: else:
last_images = last_helds["images"][my_unique_id] last_images = get_value_by_id("images", my_unique_id)
# Initialize latent # Initialize latent
latent: Tensor|None = None latent: Tensor|None = None
@@ -294,25 +466,25 @@ class TSC_KSampler:
latent = samples[0]["samples"] latent = samples[0]["samples"]
# Store the latent samples in the 'last_helds' dictionary with a unique ID # Store the latent samples in the 'last_helds' dictionary with a unique ID
last_helds["latent"][my_unique_id] = latent update_value_by_id("latent", my_unique_id, latent)
# If not in preview mode, return the results in the specified format # If not in preview mode, return the results in the specified format
if preview_image == "Disabled": if preview_image == "Disabled":
# Enable vae decode on next Hold # Enable vae decode on next Hold
last_helds["vae_decode"][my_unique_id] = True update_value_by_id("vae_decode", my_unique_id, True)
return {"ui": {"images": list()}, return {"ui": {"images": list()},
"result": (model, positive, negative, {"samples": latent}, vae, last_images,)} "result": (model, positive, negative, {"samples": latent}, vae, TSC_KSampler.empty_image,)}
else: else:
# Decode images and store # Decode images and store
images = vae.decode(latent).cpu() images = vae.decode(latent).cpu()
last_helds["images"][my_unique_id] = images update_value_by_id("images", my_unique_id, images)
# Disable vae decode on next Hold # Disable vae decode on next Hold
last_helds["vae_decode"][my_unique_id] = False update_value_by_id("vae_decode", my_unique_id, False)
# Generate image results and store # Generate image results and store
results = preview_images(images, filename_prefix) results = preview_images(images, filename_prefix)
last_helds["results"][my_unique_id] = results update_value_by_id("results", my_unique_id, results)
# Output image results to ui and node outputs # Output image results to ui and node outputs
return {"ui": {"images": results}, return {"ui": {"images": results},
@@ -326,24 +498,24 @@ class TSC_KSampler:
# If not in preview mode, return the results in the specified format # If not in preview mode, return the results in the specified format
if preview_image == "Disabled": if preview_image == "Disabled":
return {"ui": {"images": list()}, return {"ui": {"images": list()},
"result": (model, positive, negative, last_latent, vae, last_images,)} "result": (model, positive, negative, last_latent, vae, TSC_KSampler.empty_image,)}
# if preview_image == "Enabled": # if preview_image == "Enabled":
else: else:
latent = last_latent["samples"] latent = last_latent["samples"]
if last_helds["vae_decode"][my_unique_id] == True: if get_value_by_id("vae_decode", my_unique_id) == True:
# Decode images and store # Decode images and store
images = vae.decode(latent).cpu() images = vae.decode(latent).cpu()
last_helds["images"][my_unique_id] = images update_value_by_id("images", my_unique_id, images)
# Disable vae decode on next Hold # Disable vae decode on next Hold
last_helds["vae_decode"][my_unique_id] = False update_value_by_id("vae_decode", my_unique_id, False)
# Generate image results and store # Generate image results and store
results = preview_images(images, filename_prefix) results = preview_images(images, filename_prefix)
last_helds["results"][my_unique_id] = results update_value_by_id("results", my_unique_id, results)
else: else:
images = last_images images = last_images
@@ -368,6 +540,11 @@ class TSC_KSampler:
return {"ui": {"images": list()}, return {"ui": {"images": list()},
"result": (model, positive, negative, last_latent, vae, last_images,)} "result": (model, positive, negative, last_latent, vae, last_images,)}
if vae == (None,):
print('\033[31mKSampler(Efficient)[{}] Error:\033[0m VAE must be connected to use Script mode.'.format(my_unique_id))
return {"ui": {"images": list()},
"result": (model, positive, negative, last_latent, vae, last_images,)}
# Extract the 'samples' tensor from the dictionary # Extract the 'samples' tensor from the dictionary
latent_image_tensor = latent_image['samples'] latent_image_tensor = latent_image['samples']
@@ -420,12 +597,19 @@ class TSC_KSampler:
# If var_type is "Sampler", update sampler_name, scheduler, and generate labels # If var_type is "Sampler", update sampler_name, scheduler, and generate labels
elif var_type == "Sampler": elif var_type == "Sampler":
sampler_name = var[0] sampler_name = var[0]
if var[1] != None: if var[1] == "":
scheduler[0] = var[1] text = f"{sampler_name}"
else: else:
scheduler[0] = scheduler[1] if var[1] != None:
text = f"{sampler_name} ({scheduler[0]})" scheduler[0] = var[1]
else:
scheduler[0] = scheduler[1]
text = f"{sampler_name} ({scheduler[0]})"
text = text.replace("ancestral", "a").replace("uniform", "u") text = text.replace("ancestral", "a").replace("uniform", "u")
# If var_type is "Scheduler", update scheduler and generate labels
elif var_type == "Scheduler":
scheduler[0] = var
text = f"{scheduler[0]}"
# If var_type is "Denoise", update denoise and generate labels # If var_type is "Denoise", update denoise and generate labels
elif var_type == "Denoise": elif var_type == "Denoise":
denoise = var denoise = var
@@ -546,7 +730,7 @@ class TSC_KSampler:
def adjusted_font_size(text, initial_font_size, max_width): def adjusted_font_size(text, initial_font_size, max_width):
font = ImageFont.truetype('arial.ttf', initial_font_size) font = ImageFont.truetype(str(Path(font_path)), initial_font_size)
text_width, _ = font.getsize(text) text_width, _ = font.getsize(text)
if text_width > (max_width * 0.9): if text_width > (max_width * 0.9):
@@ -558,7 +742,7 @@ class TSC_KSampler:
return new_font_size return new_font_size
# Disable vae decode on next Hold # Disable vae decode on next Hold
last_helds["vae_decode"][my_unique_id] = False update_value_by_id("vae_decode", my_unique_id, False)
# Extract plot dimensions # Extract plot dimensions
num_rows = max(len(Y_value) if Y_value is not None else 0, 1) num_rows = max(len(Y_value) if Y_value is not None else 0, 1)
@@ -579,7 +763,7 @@ class TSC_KSampler:
latent_new = torch.cat(latent_new, dim=0) latent_new = torch.cat(latent_new, dim=0)
# Store latent_new as last latent # Store latent_new as last latent
last_helds["latent"][my_unique_id] = latent_new update_value_by_id("latent", my_unique_id, latent_new)
# Calculate the dimensions of the white background image # Calculate the dimensions of the white background image
border_size = max_width // 15 border_size = max_width // 15
@@ -597,7 +781,7 @@ class TSC_KSampler:
bg_height = num_rows * max_height + (num_rows - 1) * grid_spacing bg_height = num_rows * max_height + (num_rows - 1) * grid_spacing
y_offset = 0 y_offset = 0
else: else:
bg_height = num_rows * max_height + (num_rows - 1) * grid_spacing + 2.3 * border_size bg_height = num_rows * max_height + (num_rows - 1) * grid_spacing + 3 * border_size
y_offset = border_size * 3 y_offset = border_size * 3
# Create the white background image # Create the white background image
@@ -630,7 +814,7 @@ class TSC_KSampler:
d = ImageDraw.Draw(label_bg) d = ImageDraw.Draw(label_bg)
# Create the font object # Create the font object
font = ImageFont.truetype('arial.ttf', font_size) font = ImageFont.truetype(str(Path(font_path)), font_size)
# Calculate the text size and the starting position # Calculate the text size and the starting position
text_width, text_height = d.textsize(text, font=font) text_width, text_height = d.textsize(text, font=font)
@@ -662,7 +846,7 @@ class TSC_KSampler:
d = ImageDraw.Draw(label_bg) d = ImageDraw.Draw(label_bg)
# Create the font object # Create the font object
font = ImageFont.truetype('arial.ttf', font_size) font = ImageFont.truetype(str(Path(font_path)), font_size)
# Calculate the text size and the starting position # Calculate the text size and the starting position
text_width, text_height = d.textsize(text, font=font) text_width, text_height = d.textsize(text, font=font)
@@ -695,18 +879,14 @@ class TSC_KSampler:
y_offset += img.height + grid_spacing y_offset += img.height + grid_spacing
images = pil2tensor(background) images = pil2tensor(background)
last_helds["images"][my_unique_id] = images update_value_by_id("images", my_unique_id, images)
# Generate image results and store # Generate image results and store
results = preview_images(images, filename_prefix) results = preview_images(images, filename_prefix)
last_helds["results"][my_unique_id] = results update_value_by_id("results", my_unique_id, results)
# If not in preview mode, return the results in the specified format # Output image results to ui and node outputs
if preview_image == "Disabled": return {"ui": {"images": results}, "result": (model, positive, negative, {"samples": latent_new}, vae, images,)}
return {"ui": {"images": list()}, "result": (model, positive, negative, last_latent, vae, images,)}
else:
# Output image results to ui and node outputs
return {"ui": {"images": results}, "result": (model, positive, negative, {"samples": latent_new}, vae, images,)}
# TSC XY Plot # TSC XY Plot
@@ -718,6 +898,7 @@ class TSC_XYplot:
"CFG Scale 5;10;15;20\n" \ "CFG Scale 5;10;15;20\n" \
"Sampler(1) dpmpp_2s_ancestral;euler;ddim\n" \ "Sampler(1) dpmpp_2s_ancestral;euler;ddim\n" \
"Sampler(2) dpmpp_2m,karras;heun,normal\n" \ "Sampler(2) dpmpp_2m,karras;heun,normal\n" \
"Scheduler normal;simple;karras\n" \
"Denoise .3;.4;.5;.6;.7\n" \ "Denoise .3;.4;.5;.6;.7\n" \
"VAE vae_1; vae_2; vae_3" "VAE vae_1; vae_2; vae_3"
@@ -733,11 +914,11 @@ class TSC_XYplot:
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return {"required": { return {"required": {
"X_type": (["Nothing", "Latent Batch", "Seeds++ Batch", "X_type": (["Nothing", "Latent Batch", "Seeds++ Batch", "Steps", "CFG Scale",
"Steps", "CFG Scale", "Sampler", "Denoise", "VAE"],), "Sampler", "Scheduler", "Denoise", "VAE"],),
"X_value": ("STRING", {"default": "", "multiline": False}), "X_value": ("STRING", {"default": "", "multiline": False}),
"Y_type": (["Nothing", "Latent Batch", "Seeds++ Batch", "Y_type": (["Nothing", "Latent Batch", "Seeds++ Batch", "Steps", "CFG Scale",
"Steps", "CFG Scale", "Sampler", "Denoise", "VAE"],), "Sampler", "Scheduler", "Denoise", "VAE"],),
"Y_value": ("STRING", {"default": "", "multiline": False}), "Y_value": ("STRING", {"default": "", "multiline": False}),
"grid_spacing": ("INT", {"default": 0, "min": 0, "max": 500, "step": 5}), "grid_spacing": ("INT", {"default": 0, "min": 0, "max": 500, "step": 5}),
"XY_flip": (["False","True"],), "XY_flip": (["False","True"],),
@@ -791,6 +972,7 @@ class TSC_XYplot:
None if no validation was done or failed. None if no validation was done or failed.
""" """
# Seeds++ Batch
if value_type == "Seeds++ Batch": if value_type == "Seeds++ Batch":
try: try:
x = float(value) x = float(value)
@@ -812,6 +994,7 @@ class TSC_XYplot:
x = bounds["Seeds++ Batch"]["max"] x = bounds["Seeds++ Batch"]["max"]
return x return x
# Steps
elif value_type == "Steps": elif value_type == "Steps":
try: try:
x = int(value) x = int(value)
@@ -822,6 +1005,7 @@ class TSC_XYplot:
print( print(
f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid Step count.") f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid Step count.")
return None return None
# CFG Scale
elif value_type == "CFG Scale": elif value_type == "CFG Scale":
try: try:
x = float(value) x = float(value)
@@ -835,6 +1019,7 @@ class TSC_XYplot:
f"\033[31mXY Plot Error:\033[0m '{value}' is not a number between {bounds['CFG Scale']['min']}" f"\033[31mXY Plot Error:\033[0m '{value}' is not a number between {bounds['CFG Scale']['min']}"
f" and {bounds['CFG Scale']['max']} for CFG Scale.") f" and {bounds['CFG Scale']['max']} for CFG Scale.")
return None return None
# Sampler
elif value_type == "Sampler": elif value_type == "Sampler":
if isinstance(value, str) and ',' in value: if isinstance(value, str) and ',' in value:
value = tuple(map(str.strip, value.split(','))) value = tuple(map(str.strip, value.split(',')))
@@ -869,6 +1054,16 @@ class TSC_XYplot:
return None return None
else: else:
return value, None return value, None
# Scheduler
elif value_type == "Scheduler":
if value not in bounds["Scheduler"]["options"]:
valid_schedulers = '\n'.join(bounds["Scheduler"]["options"])
print(
f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid Scheduler. Valid Schedulers are:\n{valid_schedulers}")
return None
else:
return value
# Denoise
elif value_type == "Denoise": elif value_type == "Denoise":
try: try:
x = float(value) x = float(value)
@@ -882,6 +1077,7 @@ class TSC_XYplot:
f"\033[31mXY Plot Error:\033[0m '{value}' is not a number between {bounds['Denoise']['min']} " f"\033[31mXY Plot Error:\033[0m '{value}' is not a number between {bounds['Denoise']['min']} "
f"and {bounds['Denoise']['max']} for Denoise.") f"and {bounds['Denoise']['max']} for Denoise.")
return None return None
# VAE
elif value_type == "VAE": elif value_type == "VAE":
if value not in bounds["VAE"]["options"]: if value not in bounds["VAE"]["options"]:
valid_vaes = '\n'.join(bounds["VAE"]["options"]) valid_vaes = '\n'.join(bounds["VAE"]["options"])
@@ -939,6 +1135,14 @@ class TSC_XYplot:
# Reset variables to default values and return # Reset variables to default values and return
return (reset_variables(),) return (reset_variables(),)
# Clean Schedulers from Sampler data (if other type is Scheduler)
if X_type == "Sampler" and Y_type == "Scheduler":
# Clear X_value Scheduler's
X_value = [[x[0], ""] for x in X_value]
elif Y_type == "Sampler" and X_type == "Scheduler":
# Clear Y_value Scheduler's
Y_value = [[y[0], ""] for y in Y_value]
# Clean X/Y_values # Clean X/Y_values
if X_type == "Nothing" or X_type == "Latent Batch": if X_type == "Nothing" or X_type == "Latent Batch":
X_value = [None] X_value = [None]