Download efficiency_nodes.py from Outimus/efficiency-nodes-comfyui: direct link, hf CLI and curl.
- Browser
- Download file 122 kB
-
https://huggingface.co/Outimus/efficiency-nodes-comfyui/resolve/refs%2Fpr%2F1/efficiency_nodes.py
- Command line
-
hf download hf://Outimus/efficiency-nodes-comfyui@refs/pr/1/efficiency_nodes.py
-
curl -L -o efficiency_nodes.py https://huggingface.co/Outimus/efficiency-nodes-comfyui/resolve/refs%2Fpr%2F1/efficiency_nodes.py
122 kB
| # Efficiency Nodes - A collection of my ComfyUI custom nodes to help streamline workflows and reduce total node count. | |
| # by Luciano Cirino (Discord: TSC#9184) - April 2023 | |
| from comfy.sd import ModelPatcher, CLIP, VAE | |
| from nodes import common_ksampler, CLIPSetLastLayer | |
| from torch import Tensor | |
| from PIL import Image, ImageOps, ImageDraw, ImageFont | |
| from PIL.PngImagePlugin import PngInfo | |
| import numpy as np | |
| import torch | |
| import ast | |
| from pathlib import Path | |
| import os | |
| import sys | |
| import subprocess | |
| import json | |
| import folder_paths | |
| import psutil | |
| # Get the absolute path of the parent directory of the current script | |
| my_dir = os.path.dirname(os.path.abspath(__file__)) | |
| # Add the My directory path to the sys.path list | |
| sys.path.append(my_dir) | |
| # Construct the absolute path to the ComfyUI directory | |
| comfy_dir = os.path.abspath(os.path.join(my_dir, '..', '..')) | |
| # Add the ComfyUI directory path to the sys.path list | |
| sys.path.append(comfy_dir) | |
| # Construct the path to the font file | |
| font_path = os.path.join(my_dir, 'arial.ttf') | |
| # Import functions from ComfyUI | |
| import comfy.samplers | |
| import comfy.sd | |
| import comfy.utils | |
| # Import my util functions | |
| from tsc_utils import * | |
| MAX_RESOLUTION=8192 | |
| ######################################################################################################################## | |
| # TSC Efficient Loader | |
| class TSC_EfficientLoader: | |
| def INPUT_TYPES(cls): | |
| 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": -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}), | |
| "negative": ("STRING", {"default": "Negative", "multiline": True}), | |
| "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}), | |
| "batch_size": ("INT", {"default": 1, "min": 1, "max": 64})}, | |
| "optional": {"lora_stack": ("LORA_STACK", )}, | |
| "hidden": { "prompt": "PROMPT", | |
| "my_unique_id": "UNIQUE_ID",}, | |
| } | |
| RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "DEPENDENCIES",) | |
| RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "CLIP", "DEPENDENCIES", ) | |
| FUNCTION = "efficientloader" | |
| CATEGORY = "Efficiency Nodes/Loaders" | |
| def efficientloader(self, ckpt_name, vae_name, clip_skip, lora_name, lora_model_strength, lora_clip_strength, | |
| positive, negative, empty_latent_width, empty_latent_height, batch_size, lora_stack=None, | |
| prompt=None, my_unique_id=None): | |
| model: ModelPatcher | None = None | |
| clip: CLIP | None = None | |
| vae: VAE | None = None | |
| # Create Empty Latent | |
| latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]).cpu() | |
| # Clean globally stored objects | |
| globals_cleanup(prompt) | |
| # Retrieve cache numbers | |
| vae_cache, ckpt_cache, lora_cache = get_cache_numbers("Efficient Loader") | |
| if lora_name != "None": | |
| lora_params = [(lora_name, lora_model_strength, lora_clip_strength)] | |
| if lora_stack is not None: | |
| lora_params.extend(lora_stack) | |
| model, clip = load_lora(lora_params, ckpt_name, my_unique_id, cache=lora_cache, ckpt_cache=ckpt_cache, cache_overwrite=True) | |
| if vae_name == "Baked VAE": | |
| vae = get_bvae_by_ckpt_name(ckpt_name) | |
| else: | |
| model, clip, vae = load_checkpoint(ckpt_name, my_unique_id, cache=ckpt_cache, cache_overwrite=True) | |
| lora_params = None | |
| # Check for custom VAE | |
| if vae_name != "Baked VAE": | |
| vae = load_vae(vae_name, my_unique_id, cache=vae_cache, cache_overwrite=True) | |
| # Debugging | |
| ###print_loaded_objects_entries() | |
| # CLIP skip | |
| if not clip: | |
| raise Exception("No CLIP found") | |
| clip = clip.clone() | |
| clip.clip_layer(clip_skip) | |
| # Data for XY Plot | |
| dependencies = (vae_name, ckpt_name, clip, clip_skip, positive, negative, lora_params) | |
| return (model, [[clip.encode(positive), {}]], [[clip.encode(negative), {}]], {"samples":latent}, vae, clip, dependencies, ) | |
| ######################################################################################################################## | |
| # TSC LoRA Stacker | |
| class TSC_LoRA_Stacker: | |
| loras = ["None"] + folder_paths.get_filename_list("loras") | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "lora_name_1": (cls.loras,), | |
| "lora_wt_1": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "lora_name_2": (cls.loras,), | |
| "lora_wt_2": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "lora_name_3": (cls.loras,), | |
| "lora_wt_3": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})}, | |
| "optional": {"lora_stack": ("LORA_STACK",)}, | |
| } | |
| RETURN_TYPES = ("LORA_STACK",) | |
| RETURN_NAMES = ("LORA_STACK",) | |
| FUNCTION = "lora_stacker" | |
| CATEGORY = "Efficiency Nodes/Misc" | |
| def lora_stacker(self, lora_name_1, lora_wt_1, lora_name_2, lora_wt_2, lora_name_3, lora_wt_3, lora_stack=None): | |
| # Create a list of tuples using provided parameters, exclude tuples with lora_name as "None" | |
| loras = [(lora_name, lora_wt, lora_wt) for lora_name, lora_wt, lora_wt in | |
| [(lora_name_1, lora_wt_1, lora_wt_1), | |
| (lora_name_2, lora_wt_2, lora_wt_2), | |
| (lora_name_3, lora_wt_3, lora_wt_3)] | |
| if lora_name != "None"] | |
| # If lora_stack is not None, extend the loras list with lora_stack | |
| if lora_stack is not None: | |
| loras.extend([l for l in lora_stack if l[0] != "None"]) | |
| return (loras,) | |
| # TSC LoRA Stacker Advanced | |
| class TSC_LoRA_Stacker_Adv: | |
| loras = ["None"] + folder_paths.get_filename_list("loras") | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "lora_name_1": (cls.loras,), | |
| "model_str_1": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "clip_str_1": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "lora_name_2": (cls.loras,), | |
| "model_str_2": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "clip_str_2": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "lora_name_3": (cls.loras,), | |
| "model_str_3": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "clip_str_3": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})}, | |
| "optional": {"lora_stack": ("LORA_STACK",)}, | |
| } | |
| RETURN_TYPES = ("LORA_STACK",) | |
| RETURN_NAMES = ("LORA_STACK",) | |
| FUNCTION = "lora_stacker" | |
| CATEGORY = "Efficiency Nodes/Misc" | |
| def lora_stacker(self, lora_name_1, model_str_1, clip_str_1, lora_name_2, model_str_2, clip_str_2, | |
| lora_name_3, model_str_3, clip_str_3, lora_stack=None): | |
| # Create a list of tuples using provided parameters, exclude tuples with lora_name as "None" | |
| loras = [(lora_name, model_str, clip_str) for lora_name, model_str, clip_str in | |
| [(lora_name_1, model_str_1, clip_str_1), | |
| (lora_name_2, model_str_2, clip_str_2), | |
| (lora_name_3, model_str_3, clip_str_3)] | |
| if lora_name != "None"] | |
| # If lora_stack is not None, extend the loras list with lora_stack | |
| if lora_stack is not None: | |
| loras.extend([l for l in lora_stack if l[0] != "None"]) | |
| return (loras,) | |
| ######################################################################################################################## | |
| # TSC KSampler (Efficient) | |
| class TSC_KSampler: | |
| empty_image = pil2tensor(Image.new('RGBA', (1, 1), (0, 0, 0, 0))) | |
| def __init__(self): | |
| self.output_dir = os.path.join(comfy_dir, 'temp') | |
| self.type = "temp" | |
| def INPUT_TYPES(cls): | |
| return {"required": | |
| {"sampler_state": (["Sample", "Hold", "Script"], ), | |
| "model": ("MODEL",), | |
| "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), | |
| "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), | |
| "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}), | |
| "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), | |
| "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), | |
| "positive": ("CONDITIONING",), | |
| "negative": ("CONDITIONING",), | |
| "latent_image": ("LATENT",), | |
| "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), | |
| "preview_image": (["Disabled", "Enabled", "Output Only"],), | |
| }, | |
| "optional": { "optional_vae": ("VAE",), | |
| "script": ("SCRIPT",),}, | |
| "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",}, | |
| } | |
| RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "IMAGE", ) | |
| RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "IMAGE", ) | |
| OUTPUT_NODE = True | |
| FUNCTION = "sample" | |
| CATEGORY = "Efficiency Nodes/Sampling" | |
| 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, my_unique_id=None, | |
| optional_vae=(None,), script=None): | |
| # Extract node_settings from json | |
| def get_settings(): | |
| # Get the directory path of the current file | |
| my_dir = os.path.dirname(os.path.abspath(__file__)) | |
| # Construct the file path for node_settings.json | |
| settings_file = os.path.join(my_dir, 'node_settings.json') | |
| # Load the settings from the JSON file | |
| with open(settings_file, 'r') as file: | |
| node_settings = json.load(file) | |
| # Retrieve the settings | |
| kse_vae_tiled = node_settings.get("KSampler (Efficient)", {}).get('vae_tiled', False) | |
| xy_vae_tiled = node_settings.get("XY Plot", {}).get('vae_tiled', False) | |
| return kse_vae_tiled, xy_vae_tiled | |
| kse_vae_tiled, xy_vae_tiled = get_settings() | |
| # Functions for previewing images in Ksampler | |
| def map_filename(filename): | |
| prefix_len = len(os.path.basename(filename_prefix)) | |
| prefix = filename[:prefix_len + 1] | |
| try: | |
| digits = int(filename[prefix_len + 1:].split('_')[0]) | |
| except: | |
| digits = 0 | |
| return (digits, prefix) | |
| def compute_vars(input): | |
| input = input.replace("%width%", str(images[0].shape[1])) | |
| input = input.replace("%height%", str(images[0].shape[0])) | |
| return input | |
| def preview_images(images, filename_prefix): | |
| filename_prefix = compute_vars(filename_prefix) | |
| subfolder = os.path.dirname(os.path.normpath(filename_prefix)) | |
| filename = os.path.basename(os.path.normpath(filename_prefix)) | |
| full_output_folder = os.path.join(self.output_dir, subfolder) | |
| try: | |
| counter = max(filter(lambda a: a[1][:-1] == filename and a[1][-1] == "_", | |
| map(map_filename, os.listdir(full_output_folder))))[0] + 1 | |
| except ValueError: | |
| counter = 1 | |
| except FileNotFoundError: | |
| os.makedirs(full_output_folder, exist_ok=True) | |
| counter = 1 | |
| if not os.path.exists(self.output_dir): | |
| os.makedirs(self.output_dir) | |
| results = list() | |
| for image in images: | |
| i = 255. * image.cpu().numpy() | |
| img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) | |
| metadata = PngInfo() | |
| if prompt is not None: | |
| metadata.add_text("prompt", json.dumps(prompt)) | |
| if extra_pnginfo is not None: | |
| for x in extra_pnginfo: | |
| metadata.add_text(x, json.dumps(extra_pnginfo[x])) | |
| file = f"{filename}_{counter:05}_.png" | |
| img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4) | |
| results.append({ | |
| "filename": file, | |
| "subfolder": subfolder, | |
| "type": self.type | |
| }); | |
| counter += 1 | |
| 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 | |
| # Clean globally stored objects of non-existant nodes | |
| globals_cleanup(prompt) | |
| # Convert ID string to an integer | |
| my_unique_id = int(my_unique_id) | |
| # Vae input check | |
| vae = optional_vae | |
| if vae == (None,): | |
| print('\033[33mKSampler(Efficient) Warning:\033[0m No vae input detected, preview and output image disabled.\n') | |
| preview_image = "Disabled" | |
| # Init last_results | |
| if get_value_by_id("results", my_unique_id) is None: | |
| last_results = list() | |
| else: | |
| last_results = get_value_by_id("results", my_unique_id) | |
| # Init last_latent | |
| if get_value_by_id("latent", my_unique_id) is None: | |
| last_latent = latent_image | |
| else: | |
| last_latent = {"samples": None} | |
| last_latent["samples"] = get_value_by_id("latent", my_unique_id) | |
| # Init last_images | |
| if get_value_by_id("images", my_unique_id) == None: | |
| last_images = TSC_KSampler.empty_image | |
| else: | |
| last_images = get_value_by_id("images", my_unique_id) | |
| # Initialize latent | |
| latent: Tensor|None = None | |
| # Define filename_prefix | |
| filename_prefix = "KSeff_{:02d}".format(my_unique_id) | |
| # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
| # Check the current sampler state | |
| if sampler_state == "Sample": | |
| # Sample using the common KSampler function and store the samples | |
| samples = common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, | |
| latent_image, denoise=denoise) | |
| # Extract the latent samples from the returned samples dictionary | |
| latent = samples[0]["samples"] | |
| # Store the latent samples in the 'last_helds' dictionary with a unique ID | |
| update_value_by_id("latent", my_unique_id, latent) | |
| # If not in preview mode, return the results in the specified format | |
| if preview_image == "Disabled": | |
| # Enable vae decode on next Hold | |
| update_value_by_id("vae_decode", my_unique_id, True) | |
| return {"ui": {"images": list()}, | |
| "result": (model, positive, negative, {"samples": latent}, vae, TSC_KSampler.empty_image,)} | |
| else: | |
| # Decode images and store | |
| if kse_vae_tiled == False: | |
| images = vae.decode(latent).cpu() | |
| else: | |
| images = vae.decode_tiled(latent).cpu() | |
| update_value_by_id("images", my_unique_id, images) | |
| # Disable vae decode on next Hold | |
| update_value_by_id("vae_decode", my_unique_id, False) | |
| # Generate image results and store | |
| results = preview_images(images, filename_prefix) | |
| update_value_by_id("results", my_unique_id, results) | |
| # Determine what the 'images' value should be | |
| images_value = list() if preview_image == "Output Only" else results | |
| # Output image results to ui and node outputs | |
| return {"ui": {"images": images_value}, | |
| "result": (model, positive, negative, {"samples": latent}, vae, images,)} | |
| # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
| # If the sampler state is "Hold" | |
| elif sampler_state == "Hold": | |
| # If not in preview mode, return the results in the specified format | |
| if preview_image == "Disabled": | |
| return {"ui": {"images": list()}, | |
| "result": (model, positive, negative, last_latent, vae, TSC_KSampler.empty_image,)} | |
| else: | |
| latent = last_latent["samples"] | |
| if get_value_by_id("vae_decode", my_unique_id) == True: | |
| # Decode images and store | |
| if kse_vae_tiled == False: | |
| images = vae.decode(latent).cpu() | |
| else: | |
| images = vae.decode_tiled(latent).cpu() | |
| update_value_by_id("images", my_unique_id, images) | |
| # Disable vae decode on next Hold | |
| update_value_by_id("vae_decode", my_unique_id, False) | |
| # Generate image results and store | |
| results = preview_images(images, filename_prefix) | |
| update_value_by_id("results", my_unique_id, results) | |
| else: | |
| images = last_images | |
| results = last_results | |
| # Determine what the 'images' value should be | |
| images_value = list() if preview_image == "Output Only" else results | |
| # Output image results to ui and node outputs | |
| return {"ui": {"images": images_value}, | |
| "result": (model, positive, negative, {"samples": latent}, vae, images,)} | |
| # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
| elif sampler_state == "Script": | |
| # Store name of connected node to script input | |
| script_node_name, script_node_id = extract_node_info(prompt, my_unique_id, 'script') | |
| # If no valid script input connected, error out | |
| if script == None or script == (None,) or script_node_name!="XY Plot": | |
| if script_node_name!="XY Plot": | |
| print('\033[31mKSampler(Efficient) Error:\033[0m No valid script input detected') | |
| return {"ui": {"images": list()}, | |
| "result": (model, positive, negative, last_latent, vae, last_images,)} | |
| # If no vae connected, throw errors | |
| if vae == (None,): | |
| print('\033[31mKSampler(Efficient) Error:\033[0m VAE must be connected to use Script mode.') | |
| return {"ui": {"images": list()}, | |
| "result": (model, positive, negative, last_latent, vae, last_images,)} | |
| # If preview_image set to disabled, run script anyways with message | |
| if preview_image == "Disabled": | |
| print('\033[33mKSampler(Efficient) Warning:\033[0m The preview image cannot be disabled when running' | |
| ' the XY Plot script, proceeding as if it was enabled.\n') | |
| # Extract the 'samples' tensor and split it into individual image tensors | |
| image_tensors = torch.split(latent_image['samples'], 1, dim=0) | |
| # Get the shape of the first image tensor | |
| shape = image_tensors[0].shape | |
| # Extract the original height and width | |
| latent_height, latent_width = shape[2] * 8, shape[3] * 8 | |
| # Set latent only to the first latent of batch | |
| latent_image = {'samples': image_tensors[0]} | |
| #___________________________________________________________________________________________________________ | |
| # Initialize, unpack, and clean variables for the XY Plot script | |
| if script_node_name == "XY Plot": | |
| # Initialize variables | |
| vae_name = None | |
| ckpt_name = None | |
| clip = None | |
| lora_params = None | |
| positive_prompt = None | |
| negative_prompt = None | |
| clip_skip = None | |
| # Unpack script Tuple (X_type, X_value, Y_type, Y_value, grid_spacing, Y_label_orientation, dependencies) | |
| X_type, X_value, Y_type, Y_value, grid_spacing, Y_label_orientation, cache_models, xyplot_as_output_image,\ | |
| flip_xy, dependencies = script | |
| # Unpack Effficient Loader dependencies | |
| if dependencies is not None: | |
| vae_name, ckpt_name, clip, clip_skip, positive_prompt, negative_prompt, lora_params = dependencies | |
| # Helper function to process printout values | |
| def process_xy_for_print(value, replacement, type_): | |
| if isinstance(value, tuple) and type_ == "Scheduler": | |
| return value[0] # Return only the first entry of the tuple | |
| elif isinstance(value, tuple): | |
| return tuple(replacement if v is None else v for v in value) | |
| else: | |
| return replacement if value is None else value | |
| # Determine the replacements based on X_type and Y_type | |
| replacement_X = scheduler if X_type == 'Sampler' else clip_skip if X_type == 'Checkpoint' else None | |
| replacement_Y = scheduler if Y_type == 'Sampler' else clip_skip if Y_type == 'Checkpoint' else None | |
| # Process X_value and Y_value | |
| X_value_processed = [process_xy_for_print(v, replacement_X, X_type) for v in X_value] | |
| Y_value_processed = [process_xy_for_print(v, replacement_Y, Y_type) for v in Y_value] | |
| # Print XY Plot Inputs | |
| print("-" * 40) | |
| print("XY Plot Script Inputs:") | |
| print(f"(X) {X_type}: {X_value_processed}") | |
| print(f"(Y) {Y_type}: {Y_value_processed}") | |
| print("-" * 40) | |
| # If not caching models, set to 1. | |
| if cache_models == "False": | |
| vae_cache = ckpt_cache = lora_cache = 1 | |
| else: | |
| # Retrieve cache numbers | |
| vae_cache, ckpt_cache, lora_cache = get_cache_numbers("XY Plot") | |
| # Pack cache numbers in a tuple | |
| cache = (vae_cache, ckpt_cache, lora_cache) | |
| # Embedd original prompts into prompt variables | |
| positive_prompt = (positive_prompt, positive_prompt) | |
| negative_prompt = (negative_prompt, negative_prompt) | |
| #_______________________________________________________________________________________________________ | |
| #The below code will clean from the cache any ckpt/vae/lora models it will not be reusing. | |
| # Map the type names to the dictionaries | |
| dict_map = {"VAE": [], "Checkpoint": [], "LoRA": []} | |
| # Create a list of tuples with types and values | |
| type_value_pairs = [(X_type, X_value), (Y_type, Y_value)] | |
| # Iterate over type-value pairs | |
| for t, v in type_value_pairs: | |
| if t in dict_map: | |
| # Flatten the list of lists of tuples if the type is "LoRA" | |
| if t == "LoRA": | |
| dict_map[t] = [item for sublist in v for item in sublist] | |
| else: | |
| dict_map[t] = v | |
| ckpt_dict = [t[0] for t in dict_map.get("Checkpoint", [])] if dict_map.get("Checkpoint", []) else [] | |
| lora_dict = [[t,] for t in dict_map.get("LoRA", [])] if dict_map.get("LoRA", []) else [] | |
| # If both ckpt_dict and lora_dict are not empty, manipulate lora_dict as described | |
| if ckpt_dict and lora_dict: | |
| lora_dict = [(lora_params, ckpt) for ckpt in ckpt_dict for lora_params in lora_dict] | |
| # If lora_dict is not empty and ckpt_dict is empty, insert ckpt_name into each tuple in lora_dict | |
| elif lora_dict: | |
| lora_dict = [(lora_params, ckpt_name) for lora_params in lora_dict] | |
| vae_dict = dict_map.get("VAE", []) | |
| # prioritize Caching Checkpoints over LoRAs but not both. | |
| if X_type == "LoRA": | |
| ckpt_dict = [] | |
| if X_type == "Checkpoint": | |
| lora_dict = [] | |
| # Print dict_arrays for debugging | |
| ###print(f"vae_dict={vae_dict}\nckpt_dict={ckpt_dict}\nlora_dict={lora_dict}") | |
| # Clean values that won't be reused | |
| clear_cache_by_exception(script_node_id, vae_dict=vae_dict, ckpt_dict=ckpt_dict, lora_dict=lora_dict) | |
| # Print loaded_objects for debugging | |
| ###print_loaded_objects_entries() | |
| #_______________________________________________________________________________________________________ | |
| # Function that changes appropiate variables for next processed generations (also generates XY_labels) | |
| def define_variable(var_type, var, seed, steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, | |
| clip_skip, positive_prompt, negative_prompt, lora_params, var_label, num_label): | |
| # Define default max label size limit | |
| max_label_len = 36 | |
| # If var_type is "Seeds++ Batch", update var and seed, and generate labels | |
| if var_type == "Seeds++ Batch": | |
| text = f"Seed: {seed}" | |
| # If var_type is "Steps", update steps and generate labels | |
| elif var_type == "Steps": | |
| steps = var | |
| text = f"steps: {steps}" | |
| # If var_type is "CFG Scale", update cfg and generate labels | |
| elif var_type == "CFG Scale": | |
| cfg = var | |
| text = f"CFG: {round(cfg,2)}" | |
| # If var_type is "Sampler", update sampler_name, scheduler, and generate labels | |
| elif var_type == "Sampler": | |
| sampler_name = var[0] | |
| if var[1] == "": | |
| text = f"{sampler_name}" | |
| else: | |
| if var[1] != None: | |
| scheduler = (var[1], scheduler[1]) | |
| else: | |
| scheduler = (scheduler[1], scheduler[1]) | |
| text = f"{sampler_name} ({scheduler[0]})" | |
| text = text.replace("ancestral", "a").replace("uniform", "u").replace("exponential","exp") | |
| # If var_type is "Scheduler", update scheduler and generate labels | |
| elif var_type == "Scheduler": | |
| if len(var) == 2: | |
| scheduler = (var[0], scheduler[1]) | |
| text = f"{sampler_name} ({scheduler[0]})" | |
| else: | |
| scheduler = (var, scheduler[1]) | |
| text = f"{scheduler[0]}" | |
| text = text.replace("ancestral", "a").replace("uniform", "u").replace("exponential","exp") | |
| # If var_type is "Denoise", update denoise and generate labels | |
| elif var_type == "Denoise": | |
| denoise = var | |
| text = f"denoise: {round(denoise, 2)}" | |
| # If var_type is "VAE", update vae_name and generate labels | |
| elif var_type == "VAE": | |
| vae_name = var | |
| vae_filename = os.path.splitext(os.path.basename(vae_name))[0] | |
| text = f"VAE: {vae_filename}" | |
| # If var_type is "Positive Prompt S/R", update positive_prompt and generate labels | |
| elif var_type == "Positive Prompt S/R": | |
| search_txt, replace_txt = var | |
| if replace_txt != None: | |
| positive_prompt = (positive_prompt[1].replace(search_txt, replace_txt, 1), positive_prompt[1]) | |
| else: | |
| positive_prompt = (positive_prompt[1], positive_prompt[1]) | |
| replace_txt = search_txt | |
| text = f"{replace_txt}" | |
| # If var_type is "Negative Prompt S/R", update negative_prompt and generate labels | |
| elif var_type == "Negative Prompt S/R": | |
| search_txt, replace_txt = var | |
| if replace_txt: | |
| negative_prompt = (negative_prompt[1].replace(search_txt, replace_txt, 1), negative_prompt[1]) | |
| else: | |
| negative_prompt = (negative_prompt[1], negative_prompt[1]) | |
| replace_txt = search_txt | |
| text = f"(-) {replace_txt}" | |
| # If var_type is "Checkpoint", update model and clip (if needed) and generate labels | |
| elif var_type == "Checkpoint": | |
| ckpt_name = var[0] | |
| if var[1] == None: | |
| clip_skip = (clip_skip[1],clip_skip[1]) | |
| else: | |
| clip_skip = (var[1],clip_skip[1]) | |
| ckpt_filename = os.path.splitext(os.path.basename(ckpt_name))[0] | |
| text = f"{ckpt_filename}" | |
| elif var_type == "Clip Skip": | |
| clip_skip = (var, clip_skip[1]) | |
| text = f"Clip Skip ({clip_skip[0]})" | |
| elif var_type == "LoRA": | |
| lora_params = var | |
| max_label_len = 30 + (12 * (len(lora_params)-1)) | |
| if len(lora_params) == 1: | |
| lora_name, lora_model_wt, lora_clip_wt = lora_params[0] | |
| lora_filename = os.path.splitext(os.path.basename(lora_name))[0] | |
| lora_model_wt = format(float(lora_model_wt), ".2f").rstrip('0').rstrip('.') | |
| lora_clip_wt = format(float(lora_clip_wt), ".2f").rstrip('0').rstrip('.') | |
| lora_filename = lora_filename[:max_label_len - len(f"LoRA: ({lora_model_wt})")] | |
| if lora_model_wt == lora_clip_wt: | |
| text = f"LoRA: {lora_filename}({lora_model_wt})" | |
| else: | |
| text = f"LoRA: {lora_filename}({lora_model_wt},{lora_clip_wt})" | |
| elif len(lora_params) > 1: | |
| lora_filenames = [os.path.splitext(os.path.basename(lora_name))[0] for lora_name, _, _ in lora_params] | |
| lora_details = [(format(float(lora_model_wt), ".2f").rstrip('0').rstrip('.'), | |
| format(float(lora_clip_wt), ".2f").rstrip('0').rstrip('.')) for _, lora_model_wt, lora_clip_wt in lora_params] | |
| non_name_length = sum(len(f"({lora_details[i][0]},{lora_details[i][1]})") + 2 for i in range(len(lora_params))) | |
| available_space = max_label_len - non_name_length | |
| max_name_length = available_space // len(lora_params) | |
| lora_filenames = [filename[:max_name_length] for filename in lora_filenames] | |
| text_elements = [f"{lora_filename}({lora_details[i][0]})" if lora_details[i][0] == lora_details[i][1] else f"{lora_filename}({lora_details[i][0]},{lora_details[i][1]})" for i, lora_filename in enumerate(lora_filenames)] | |
| text = " ".join(text_elements) | |
| def truncate_texts(texts, num_label, max_label_len): | |
| truncate_length = max(min(max(len(text) for text in texts), max_label_len), 24) | |
| return [text if len(text) <= truncate_length else text[:truncate_length] + "..." for text in | |
| texts] | |
| # Add the generated text to var_label if it's not full | |
| if len(var_label) < num_label: | |
| var_label.append(text) | |
| # If var_type VAE , truncate entries in the var_label list when it's full | |
| if len(var_label) == num_label and (var_type == "VAE" or var_type == "Checkpoint" or var_type == "LoRA"): | |
| var_label = truncate_texts(var_label, num_label, max_label_len) | |
| # Return the modified variables | |
| return steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip, \ | |
| positive_prompt, negative_prompt, lora_params, var_label | |
| # _______________________________________________________________________________________________________ | |
| # The function below is used to smartly load Checkpoint/LoRA/VAE models between generations. | |
| def define_model(model, clip, positive, negative, positive_prompt, negative_prompt, clip_skip, vae, | |
| vae_name, ckpt_name, lora_params, index, types, script_node_id, cache): | |
| # Encode prompt and apply clip_skip. Return new conditioning. | |
| def encode_prompt(positive_prompt, negative_prompt, clip, clip_skip): | |
| clip = CLIPSetLastLayer().set_last_layer(clip, clip_skip)[0] | |
| return [[clip.encode(positive_prompt), {}]], [[clip.encode(negative_prompt), {}]] | |
| # Variable to track wether to encode prompt or not | |
| encode = False | |
| # Unpack types tuple | |
| X_type, Y_type = types | |
| # Note: Index is held at 0 when Y_type == "Nothing" | |
| # Load VAE if required | |
| if (X_type == "VAE" and index == 0) or Y_type == "VAE": | |
| vae = load_vae(vae_name, script_node_id, cache=cache[0]) | |
| # Load Checkpoint if required. If Y_type is LoRA, required models will be loaded by load_lora func. | |
| if (X_type == "Checkpoint" and index == 0 and Y_type != "LoRA"): | |
| if lora_params is None: | |
| model, clip, _ = load_checkpoint(ckpt_name, script_node_id, output_vae=False, cache=cache[1]) | |
| else: # Load Efficient Loader LoRA | |
| model, clip = load_lora(lora_params, ckpt_name, script_node_id, | |
| cache=None, ckpt_cache=cache[1]) | |
| encode = True | |
| # Load LoRA if required | |
| elif (X_type == "LoRA" and index == 0): | |
| # Don't cache Checkpoints | |
| model, clip = load_lora(lora_params, ckpt_name, script_node_id, cache=cache[2]) | |
| encode = True | |
| elif Y_type == "LoRA": # X_type must be Checkpoint, so cache those as defined | |
| model, clip = load_lora(lora_params, ckpt_name, script_node_id, | |
| cache=None, ckpt_cache=cache[1]) | |
| encode = True | |
| # Encode Prompt if required | |
| prompt_types = ["Positive Prompt S/R", "Negative Prompt S/R", "Clip Skip"] | |
| if (X_type in prompt_types and index == 0) or Y_type in prompt_types: | |
| encode = True | |
| # Encode prompt if needed | |
| if encode == True: | |
| positive, negative = encode_prompt(positive_prompt[0], negative_prompt[0], clip, clip_skip) | |
| return model, positive, negative, vae | |
| # ______________________________________________________________________________________________________ | |
| # The below function is used to generate the results based on all the processed variables | |
| def process_values(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, | |
| denoise, vae, latent_list=[], image_tensor_list=[], image_pil_list=[]): | |
| # Sample | |
| samples = common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, | |
| latent_image, denoise=denoise) | |
| # Decode images and store | |
| latent = samples[0]["samples"] | |
| # Add the latent tensor to the tensors list | |
| latent_list.append(latent) | |
| # Decode the latent tensor | |
| if xy_vae_tiled == False: | |
| image = vae.decode(latent).cpu() | |
| else: | |
| image = vae.decode_tiled(latent).cpu() | |
| # Add the resulting image tensor to image_tensor_list | |
| image_tensor_list.append(image) | |
| # Convert the image from tensor to PIL Image and add it to the image_pil_list | |
| image_pil_list.append(tensor2pil(image)) | |
| # Return the touched variables | |
| return latent_list, image_tensor_list, image_pil_list | |
| # ______________________________________________________________________________________________________ | |
| # The below section is the heart of the XY Plot image generation | |
| # Initiate Plot label text variables X/Y_label | |
| X_label = [] | |
| Y_label = [] | |
| # Seed_updated for "Seeds++ Batch" incremental seeds | |
| seed_updated = seed | |
| # Store the KSamplers original scheduler inside the same scheduler variable | |
| scheduler = (scheduler, scheduler) | |
| # Store the Eff Loaders original clip_skip inside the same clip_skip variable | |
| clip_skip = (clip_skip, clip_skip) | |
| # Store types in a Tuple for easy function passing | |
| types = (X_type, Y_type) | |
| # Fill Plot Rows (X) | |
| for X_index, X in enumerate(X_value): | |
| # Seed control based on loop index during Batch | |
| if X_type == "Seeds++ Batch": | |
| # Update seed based on the inner loop index | |
| seed_updated = seed + X_index | |
| # Define X parameters and generate labels | |
| steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip, positive_prompt, negative_prompt, \ | |
| lora_params, X_label = \ | |
| define_variable(X_type, X, seed_updated, steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, | |
| clip_skip, positive_prompt, negative_prompt, lora_params, X_label, len(X_value)) | |
| if X_type != "Nothing" and Y_type == "Nothing": | |
| # Models & Conditionings | |
| model, positive, negative , vae = \ | |
| define_model(model, clip, positive, negative, positive_prompt, negative_prompt, clip_skip[0], vae, | |
| vae_name, ckpt_name, lora_params, 0, types, script_node_id, cache) | |
| # Generate Results | |
| latent_list, image_tensor_list, image_pil_list = \ | |
| process_values(model, seed_updated, steps, cfg, sampler_name, scheduler[0], | |
| positive, negative, latent_image, denoise, vae) | |
| elif X_type != "Nothing" and Y_type != "Nothing": | |
| # Seed control based on loop index during Batch | |
| for Y_index, Y in enumerate(Y_value): | |
| if Y_type == "Seeds++ Batch": | |
| # Update seed based on the inner loop index | |
| seed_updated = seed + Y_index | |
| # Define Y parameters and generate labels | |
| steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip, positive_prompt, negative_prompt, lora_params, Y_label = \ | |
| define_variable(Y_type, Y, seed_updated, steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, | |
| clip_skip, positive_prompt, negative_prompt, lora_params, Y_label, len(Y_value)) | |
| # Models & Conditionings | |
| model, positive, negative, vae = \ | |
| define_model(model, clip, positive, negative, positive_prompt, negative_prompt, clip_skip[0], vae, | |
| vae_name, ckpt_name, lora_params, Y_index, types, script_node_id, cache) | |
| # Generate Results | |
| latent_list, image_tensor_list, image_pil_list = \ | |
| process_values(model, seed_updated, steps, cfg, sampler_name, scheduler[0], | |
| positive, negative, latent_image, denoise, vae) | |
| # Clean up cache | |
| if cache_models == "False": | |
| clear_cache_by_exception(script_node_id, vae_dict=[], ckpt_dict=[], lora_dict=[]) | |
| # | |
| else: | |
| # Prioritrize Caching Checkpoints over LoRAs. | |
| if X_type == "LoRA": | |
| clear_cache_by_exception(script_node_id, ckpt_dict=[]) | |
| elif X_type == "Checkpoint": | |
| clear_cache_by_exception(script_node_id, lora_dict=[]) | |
| # ______________________________________________________________________________________________________ | |
| def print_plot_variables(X_type, Y_type, X_value, Y_value, seed, ckpt_name, lora_params, | |
| vae_name, clip_skip, steps, cfg, sampler_name, scheduler, denoise, | |
| num_rows, num_cols, latent_height, latent_width): | |
| print("-" * 40) # Print an empty line followed by a separator line | |
| print("\033[32mXY Plot Results:\033[0m") | |
| def get_vae_name(X_type, Y_type, X_value, Y_value, vae_name): | |
| if X_type == "VAE": | |
| vae_name = ", ".join(map(lambda x: os.path.splitext(os.path.basename(str(x)))[0], X_value)) | |
| elif Y_type == "VAE": | |
| vae_name = ", ".join(map(lambda y: os.path.splitext(os.path.basename(str(y)))[0], Y_value)) | |
| else: | |
| vae_name = os.path.splitext(os.path.basename(str(vae_name)))[0] | |
| return vae_name | |
| def get_clip_skip(X_type, Y_type, X_value, Y_value, clip_skip): | |
| if X_type == "Clip Skip": | |
| clip_skip = ", ".join(map(str, X_value)) | |
| elif Y_type == "Clip Skip": | |
| clip_skip = ", ".join(map(str, Y_value)) | |
| else: | |
| clip_skip = clip_skip[1] | |
| return clip_skip | |
| def get_checkpoint_name(ckpt_type, ckpt_values, clip_skip_type, clip_skip_values, ckpt_name, clip_skip): | |
| if ckpt_type == "Checkpoint": | |
| if clip_skip_type == "Clip Skip": | |
| ckpt_name = ", ".join([os.path.splitext(os.path.basename(str(ckpt[0])))[0] for ckpt in ckpt_values]) | |
| else: | |
| ckpt_name = ", ".join([f"{os.path.splitext(os.path.basename(str(ckpt[0])))[0]}({str(ckpt[1]) if ckpt[1] is not None else str(clip_skip_values)})" | |
| for ckpt in ckpt_values]) | |
| clip_skip = "_" | |
| else: | |
| ckpt_name = os.path.splitext(os.path.basename(str(ckpt_name)))[0] | |
| return ckpt_name, clip_skip | |
| def get_lora_name(X_type, Y_type, X_value, Y_value, lora_params=None): | |
| if X_type != "LoRA" and Y_type != "LoRA": | |
| if lora_params: | |
| return f"[{', '.join([f'{os.path.splitext(os.path.basename(name))[0]}({round(model_wt, 3)},{round(clip_wt, 3)})' for name, model_wt, clip_wt in lora_params])}]" | |
| else: | |
| return None | |
| else: | |
| return get_lora_sublist_name(X_type, | |
| X_value) if X_type == "LoRA" else get_lora_sublist_name(Y_type, Y_value) if Y_type == "LoRA" else None | |
| def get_lora_sublist_name(lora_type, lora_value): | |
| return ", ".join([ | |
| f"[{', '.join([f'{os.path.splitext(os.path.basename(str(x[0])))[0]}({round(x[1], 3)},{round(x[2], 3)})' for x in sublist])}]" | |
| for sublist in lora_value]) | |
| # use these functions: | |
| ckpt_type, clip_skip_type = (X_type, Y_type) if X_type in ["Checkpoint", "Clip Skip"] else (Y_type, X_type) | |
| ckpt_values, clip_skip_values = (X_value, Y_value) if X_type in ["Checkpoint", "Clip Skip"] else (Y_value, X_value) | |
| clip_skip = get_clip_skip(X_type, Y_type, X_value, Y_value, clip_skip) | |
| ckpt_name, clip_skip = get_checkpoint_name(ckpt_type, ckpt_values, clip_skip_type, clip_skip_values, ckpt_name, clip_skip) | |
| vae_name = get_vae_name(X_type, Y_type, X_value, Y_value, vae_name) | |
| lora_name = get_lora_name(X_type, Y_type, X_value, Y_value, lora_params) | |
| seed_list = [seed + x for x in X_value] if X_type == "Seeds++ Batch" else\ | |
| [seed + y for y in Y_value] if Y_type == "Seeds++ Batch" else [seed] | |
| seed = ", ".join(map(str, seed_list)) | |
| steps = ", ".join(map(str, X_value)) if X_type == "Steps" else ", ".join( | |
| map(str, Y_value)) if Y_type == "Steps" else steps | |
| cfg = ", ".join(map(str, X_value)) if X_type == "CFG Scale" else ", ".join( | |
| map(str, Y_value)) if Y_type == "CFG Scale" else cfg | |
| if X_type == "Sampler": | |
| if Y_type == "Scheduler": | |
| sampler_name = ", ".join([f"{x[0]}" for x in X_value]) | |
| scheduler = ", ".join([f"{y}" for y in Y_value]) | |
| else: | |
| sampler_name = ", ".join( | |
| [f"{x[0]}({x[1] if x[1] != '' and x[1] is not None else scheduler[1]})" for x in X_value]) | |
| scheduler = "_" | |
| elif Y_type == "Sampler": | |
| if X_type == "Scheduler": | |
| sampler_name = ", ".join([f"{y[0]}" for y in Y_value]) | |
| scheduler = ", ".join([f"{x}" for x in X_value]) | |
| else: | |
| sampler_name = ", ".join( | |
| [f"{y[0]}({y[1] if y[1] != '' and y[1] is not None else scheduler[1]})" for y in Y_value]) | |
| scheduler = "_" | |
| else: | |
| scheduler = ", ".join([str(x[0]) if isinstance(x, tuple) else str(x) for x in X_value]) if X_type == "Scheduler" else \ | |
| ", ".join([str(y[0]) if isinstance(y, tuple) else str(y) for y in Y_value]) if Y_type == "Scheduler" else scheduler[0] | |
| denoise = ", ".join(map(str, X_value)) if X_type == "Denoise" else ", ".join( | |
| map(str, Y_value)) if Y_type == "Denoise" else denoise | |
| # Printouts | |
| print(f"img_count: {len(X_value)*len(Y_value)}") | |
| print(f"img_dims: {latent_height} x {latent_width}") | |
| print(f"plot_dim: {num_cols} x {num_rows}") | |
| if clip_skip == "_": | |
| print(f"ckpt(clipskip): {ckpt_name if ckpt_name is not None else ''}") | |
| else: | |
| print(f"ckpt: {ckpt_name if ckpt_name is not None else ''}") | |
| print(f"clip_skip: {clip_skip if clip_skip is not None else ''}") | |
| if lora_name: | |
| print(f"lora(mod,clip): {lora_name if lora_name is not None else ''}") | |
| print(f"vae: {vae_name if vae_name is not None else ''}") | |
| print(f"seed: {seed}") | |
| print(f"steps: {steps}") | |
| print(f"cfg: {cfg}") | |
| if scheduler == "_": | |
| print(f"sampler(schr): {sampler_name}") | |
| else: | |
| print(f"sampler: {sampler_name}") | |
| print(f"scheduler: {scheduler}") | |
| print(f"denoise: {denoise}") | |
| if X_type == "Positive Prompt S/R" or Y_type == "Positive Prompt S/R": | |
| positive_prompt = ", ".join([str(x[0]) if i == 0 else str(x[1]) for i, x in enumerate( | |
| X_value)]) if X_type == "Positive Prompt S/R" else ", ".join( | |
| [str(y[0]) if i == 0 else str(y[1]) for i, y in | |
| enumerate(Y_value)]) if Y_type == "Positive Prompt S/R" else positive_prompt | |
| print(f"+prompt_s/r: {positive_prompt}") | |
| if X_type == "Negative Prompt S/R" or Y_type == "Negative Prompt S/R": | |
| negative_prompt = ", ".join([str(x[0]) if i == 0 else str(x[1]) for i, x in enumerate( | |
| X_value)]) if X_type == "Negative Prompt S/R" else ", ".join( | |
| [str(y[0]) if i == 0 else str(y[1]) for i, y in | |
| enumerate(Y_value)]) if Y_type == "Negative Prompt S/R" else negative_prompt | |
| print(f"-prompt_s/r: {negative_prompt}") | |
| # ______________________________________________________________________________________________________ | |
| def adjusted_font_size(text, initial_font_size, latent_width): | |
| font = ImageFont.truetype(str(Path(font_path)), initial_font_size) | |
| text_width = font.getlength(text) | |
| if text_width > (latent_width * 0.9): | |
| scaling_factor = 0.9 # A value less than 1 to shrink the font size more aggressively | |
| new_font_size = int(initial_font_size * (latent_width / text_width) * scaling_factor) | |
| else: | |
| new_font_size = initial_font_size | |
| return new_font_size | |
| # ______________________________________________________________________________________________________ | |
| # Disable vae decode on next Hold | |
| update_value_by_id("vae_decode", my_unique_id, False) | |
| def rearrange_list_A(arr, num_cols, num_rows): | |
| new_list = [] | |
| for i in range(num_rows): | |
| for j in range(num_cols): | |
| index = j * num_rows + i | |
| new_list.append(arr[index]) | |
| return new_list | |
| def rearrange_list_B(arr, num_rows, num_cols): | |
| new_list = [] | |
| for i in range(num_rows): | |
| for j in range(num_cols): | |
| index = i * num_cols + j | |
| new_list.append(arr[index]) | |
| return new_list | |
| # Extract plot dimensions | |
| num_rows = max(len(Y_value) if Y_value is not None else 0, 1) | |
| num_cols = max(len(X_value) if X_value is not None else 0, 1) | |
| # Flip X & Y results back if flipped earlier (for Checkpoint/LoRA For loop optimizations) | |
| if flip_xy == True: | |
| X_type, Y_type = Y_type, X_type | |
| X_value, Y_value = Y_value, X_value | |
| X_label, Y_label = Y_label, X_label | |
| num_rows, num_cols = num_cols, num_rows | |
| image_pil_list = rearrange_list_A(image_pil_list, num_rows, num_cols) | |
| else: | |
| image_pil_list = rearrange_list_B(image_pil_list, num_rows, num_cols) | |
| image_tensor_list = rearrange_list_A(image_tensor_list, num_cols, num_rows) | |
| latent_list = rearrange_list_A(latent_list, num_cols, num_rows) | |
| # Print XY Plot Results | |
| print_plot_variables(X_type, Y_type, X_value, Y_value, seed, ckpt_name, lora_params, vae_name, | |
| clip_skip, steps, cfg, sampler_name, scheduler, denoise, | |
| num_rows, num_cols, latent_height, latent_width) | |
| # Concatenate the tensors along the first dimension (dim=0) | |
| latent_list = torch.cat(latent_list, dim=0) | |
| # Store latent_list as last latent | |
| update_value_by_id("latent", my_unique_id, latent_list) | |
| # Calculate the dimensions of the white background image | |
| border_size_top = latent_width // 15 | |
| # Longest Y-label length | |
| if len(Y_label) > 0: | |
| Y_label_longest = max(len(s) for s in Y_label) | |
| else: | |
| # Handle the case when the sequence is empty | |
| Y_label_longest = 0 # or any other appropriate value | |
| Y_label_scale = min(Y_label_longest + 4,24) / 24 | |
| if Y_label_orientation == "Vertical": | |
| border_size_left = border_size_top | |
| else: # Assuming Y_label_orientation is "Horizontal" | |
| # border_size_left is now min(latent_width, latent_height) plus 20% of the difference between the two | |
| border_size_left = min(latent_width, latent_height) + int(0.2 * abs(latent_width - latent_height)) | |
| border_size_left = int(border_size_left * Y_label_scale) | |
| # Modify the border size, background width and x_offset initialization based on Y_type and Y_label_orientation | |
| if Y_type == "Nothing": | |
| bg_width = num_cols * latent_width + (num_cols - 1) * grid_spacing | |
| x_offset_initial = 0 | |
| else: | |
| if Y_label_orientation == "Vertical": | |
| bg_width = num_cols * latent_width + (num_cols - 1) * grid_spacing + 3 * border_size_left | |
| x_offset_initial = border_size_left * 3 | |
| else: # Assuming Y_label_orientation is "Horizontal" | |
| bg_width = num_cols * latent_width + (num_cols - 1) * grid_spacing + border_size_left | |
| x_offset_initial = border_size_left | |
| # Modify the background height based on X_type | |
| if X_type == "Nothing": | |
| bg_height = num_rows * latent_height + (num_rows - 1) * grid_spacing | |
| y_offset = 0 | |
| else: | |
| bg_height = num_rows * latent_height + (num_rows - 1) * grid_spacing + 3 * border_size_top | |
| y_offset = border_size_top * 3 | |
| # Create the white background image | |
| background = Image.new('RGBA', (int(bg_width), int(bg_height)), color=(255, 255, 255, 255)) | |
| for row in range(num_rows): | |
| # Initialize the X_offset | |
| x_offset = x_offset_initial | |
| for col in range(num_cols): | |
| # Calculate the index for image_pil_list | |
| index = col * num_rows + row | |
| img = image_pil_list[index] | |
| # Paste the image | |
| background.paste(img, (x_offset, y_offset)) | |
| if row == 0 and X_type != "Nothing": | |
| # Assign text | |
| text = X_label[col] | |
| # Add the corresponding X_value as a label above the image | |
| initial_font_size = int(48 * img.width / 512) | |
| font_size = adjusted_font_size(text, initial_font_size, img.width) | |
| label_height = int(font_size*1.5) | |
| # Create a white background label image | |
| label_bg = Image.new('RGBA', (img.width, label_height), color=(255, 255, 255, 0)) | |
| d = ImageDraw.Draw(label_bg) | |
| # Create the font object | |
| font = ImageFont.truetype(str(Path(font_path)), font_size) | |
| # Calculate the text size and the starting position | |
| _, _, text_width, text_height = d.textbbox([0,0], text, font=font) | |
| text_x = (img.width - text_width) // 2 | |
| text_y = (label_height - text_height) // 2 | |
| # Add the text to the label image | |
| d.text((text_x, text_y), text, fill='black', font=font) | |
| # Calculate the available space between the top of the background and the top of the image | |
| available_space = y_offset - label_height | |
| # Calculate the new Y position for the label image | |
| label_y = available_space // 2 | |
| # Paste the label image above the image on the background using alpha_composite() | |
| background.alpha_composite(label_bg, (x_offset, label_y)) | |
| if col == 0 and Y_type != "Nothing": | |
| # Assign text | |
| text = Y_label[row] | |
| # Add the corresponding Y_value as a label to the left of the image | |
| if Y_label_orientation == "Vertical": | |
| initial_font_size = int(48 * latent_width / 512) # Adjusting this to be same as X_label size | |
| font_size = adjusted_font_size(text, initial_font_size, latent_width) | |
| else: # Assuming Y_label_orientation is "Horizontal" | |
| initial_font_size = int(48 * (border_size_left/Y_label_scale) / 512) # Adjusting this to be same as X_label size | |
| font_size = adjusted_font_size(text, initial_font_size, int(border_size_left/Y_label_scale)) | |
| # Create a white background label image | |
| label_bg = Image.new('RGBA', (img.height, int(font_size*1.2)), color=(255, 255, 255, 0)) | |
| d = ImageDraw.Draw(label_bg) | |
| # Create the font object | |
| font = ImageFont.truetype(str(Path(font_path)), font_size) | |
| # Calculate the text size and the starting position | |
| _, _, text_width, text_height = d.textbbox([0,0], text, font=font) | |
| text_x = (img.height - text_width) // 2 | |
| text_y = (font_size - text_height) // 2 | |
| # Add the text to the label image | |
| d.text((text_x, text_y), text, fill='black', font=font) | |
| # Rotate the label_bg 90 degrees counter-clockwise only if Y_label_orientation is "Vertical" | |
| if Y_label_orientation == "Vertical": | |
| label_bg = label_bg.rotate(90, expand=True) | |
| # Calculate the available space between the left of the background and the left of the image | |
| available_space = x_offset - label_bg.width | |
| # Calculate the new X position for the label image | |
| label_x = available_space // 2 | |
| # Calculate the Y position for the label image based on its orientation | |
| if Y_label_orientation == "Vertical": | |
| label_y = y_offset + (img.height - label_bg.height) // 2 | |
| else: # Assuming Y_label_orientation is "Horizontal" | |
| label_y = y_offset + img.height - (img.height - label_bg.height) // 2 | |
| # Paste the label image to the left of the image on the background using alpha_composite() | |
| background.alpha_composite(label_bg, (label_x, label_y)) | |
| # Update the x_offset | |
| x_offset += img.width + grid_spacing | |
| # Update the y_offset | |
| y_offset += img.height + grid_spacing | |
| images = pil2tensor(background) | |
| # Generate image results and store | |
| results = preview_images(images, filename_prefix) | |
| update_value_by_id("results", my_unique_id, results) | |
| # Squeeze and Stack the tensors, and store results | |
| if xyplot_as_output_image == False: | |
| image_tensor_list = torch.stack([tensor.squeeze() for tensor in image_tensor_list]) | |
| else: | |
| image_tensor_list = images | |
| update_value_by_id("images", my_unique_id, image_tensor_list) | |
| # Print cache if set to true | |
| if cache_models == "True": | |
| print_loaded_objects_entries(script_node_id, prompt) | |
| print("-" * 40) # Print an empty line followed by a separator line | |
| images = list() if preview_image == "Output Only" else results | |
| return { | |
| "ui": {"images": images}, | |
| "result": (model, positive, negative, {"samples": latent_list}, vae, image_tensor_list,) | |
| } | |
| ######################################################################################################################## | |
| # TSC XY Plot | |
| class TSC_XYplot: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "grid_spacing": ("INT", {"default": 0, "min": 0, "max": 500, "step": 5}), | |
| "XY_flip": (["False","True"],), | |
| "Y_label_orientation": (["Horizontal", "Vertical"],), | |
| "cache_models": (["True", "False"],), | |
| "ksampler_output_image": (["Plot", "Images"],),}, | |
| "optional": { | |
| "dependencies": ("DEPENDENCIES", ), | |
| "X": ("XY", ), | |
| "Y": ("XY", ),}, | |
| } | |
| RETURN_TYPES = ("SCRIPT",) | |
| RETURN_NAMES = ("SCRIPT",) | |
| FUNCTION = "XYplot" | |
| CATEGORY = "Efficiency Nodes/XY Plot" | |
| def XYplot(self, grid_spacing, XY_flip, Y_label_orientation, cache_models, ksampler_output_image, dependencies=None, X=None, Y=None): | |
| # Unpack X & Y Tuples if connected | |
| if X != None: | |
| X_type, X_value = X | |
| else: | |
| X_type = "Nothing" | |
| X_value = [""] | |
| if Y != None: | |
| Y_type, Y_value = Y | |
| else: | |
| Y_type = "Nothing" | |
| Y_value = [""] | |
| # If types are the same exit. If one isn't "Nothing", print error | |
| if (X_type == Y_type): | |
| if X_type != "Nothing": | |
| print(f"\033[31mXY Plot Error:\033[0m X and Y input types must be different.") | |
| return (None,) | |
| # Check that dependencies is connected for Checkpoint and LoRA plots | |
| types = ("Checkpoint", "LoRA", "Positive Prompt S/R", "Negative Prompt S/R") | |
| if X_type in types or Y_type in types: | |
| if dependencies == None: # Not connected | |
| print(f"\033[31mXY Plot Error:\033[0m The dependencies input must be connected for certain plot types.") | |
| # Return None | |
| return (None,) | |
| # Define X/Y_values for "Seeds++ Batch" | |
| if X_type == "Seeds++ Batch": | |
| X_value = [i for i in range(X_value[0])] | |
| if Y_type == "Seeds++ Batch": | |
| Y_value = [i for i in range(Y_value[0])] | |
| # 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] | |
| # Embed information into "Scheduler" X/Y_values for text label | |
| if X_type == "Scheduler" and Y_type != "Sampler": | |
| # X_value second tuple value of each array entry = None | |
| X_value = [(x, None) for x in X_value] | |
| if Y_type == "Scheduler" and X_type != "Sampler": | |
| # Y_value second tuple value of each array entry = None | |
| Y_value = [(y, None) for y in Y_value] | |
| # Optimize image generation by prioritizing Checkpoint>LoRA>VAE>PromptSR as X in For Loop. Flip back when done. | |
| if Y_type == "Checkpoint" or \ | |
| Y_type == "LoRA" and X_type not in {"Checkpoint"} or \ | |
| Y_type == "VAE" and X_type not in {"Checkpoint", "LoRA"} or \ | |
| Y_type == "Positive Prompt S/R" and X_type not in {"Checkpoint", "LoRA", "VAE", | |
| "Negative Prompt S/R"} or \ | |
| Y_type == "Negative Prompt S/R" and X_type not in {"Checkpoint", "LoRA", "VAE", | |
| "Positive Prompt S/R"} or \ | |
| X_type == "Nothing" and Y_type != "Nothing": | |
| flip_xy = True | |
| X_type, Y_type = Y_type, X_type | |
| X_value, Y_value = Y_value, X_value | |
| else: | |
| flip_xy = False | |
| # Flip X and Y | |
| if XY_flip == "True": | |
| X_type, Y_type = Y_type, X_type | |
| X_value, Y_value = Y_value, X_value | |
| # Define Ksampler output image behavior | |
| xyplot_as_output_image = ksampler_output_image == "Plot" | |
| return ((X_type, X_value, Y_type, Y_value, grid_spacing, Y_label_orientation, cache_models, | |
| xyplot_as_output_image, flip_xy, dependencies),) | |
| # TSC XY Plot: Seeds Values | |
| class TSC_XYplot_SeedsBatch: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "batch_count": ("INT", {"default": 1, "min": 0, "max": 50}),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, batch_count): | |
| if batch_count == 0: | |
| return (None,) | |
| xy_type = "Seeds++ Batch" | |
| xy_value = [batch_count] | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: Step Values | |
| class TSC_XYplot_Steps: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "select_count": ("INT", {"default": 0, "min": 0, "max": 5}), | |
| "steps_1": ("INT", {"default": 20, "min": 1, "max": 10000}), | |
| "steps_2": ("INT", {"default": 20, "min": 1, "max": 10000}), | |
| "steps_3": ("INT", {"default": 20, "min": 1, "max": 10000}), | |
| "steps_4": ("INT", {"default": 20, "min": 1, "max": 10000}), | |
| "steps_5": ("INT", {"default": 20, "min": 1, "max": 10000}),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, select_count, steps_1, steps_2, steps_3, steps_4, steps_5): | |
| xy_type = "Steps" | |
| xy_value = [step for idx, step in enumerate([steps_1, steps_2, steps_3, steps_4, steps_5], start=1) if | |
| idx <= select_count] | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: CFG Values | |
| class TSC_XYplot_CFG: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "select_count": ("INT", {"default": 0, "min": 0, "max": 5}), | |
| "cfg_1": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}), | |
| "cfg_2": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}), | |
| "cfg_3": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}), | |
| "cfg_4": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}), | |
| "cfg_5": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, select_count, cfg_1, cfg_2, cfg_3, cfg_4, cfg_5): | |
| xy_type = "CFG Scale" | |
| xy_value = [cfg for idx, cfg in enumerate([cfg_1, cfg_2, cfg_3, cfg_4, cfg_5], start=1) if idx <= select_count] | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: Sampler Values | |
| class TSC_XYplot_Sampler: | |
| samplers = ["None"] + comfy.samplers.KSampler.SAMPLERS | |
| schedulers = ["None"] + comfy.samplers.KSampler.SCHEDULERS | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "sampler_1": (cls.samplers,), | |
| "scheduler_1": (cls.schedulers,), | |
| "sampler_2": (cls.samplers,), | |
| "scheduler_2": (cls.schedulers,), | |
| "sampler_3": (cls.samplers,), | |
| "scheduler_3": (cls.schedulers,), | |
| "sampler_4": (cls.samplers,), | |
| "scheduler_4": (cls.schedulers,), | |
| "sampler_5": (cls.samplers,), | |
| "scheduler_5": (cls.schedulers,),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, sampler_1, scheduler_1, sampler_2, scheduler_2, sampler_3, scheduler_3, | |
| sampler_4, scheduler_4, sampler_5, scheduler_5): | |
| samplers = [sampler_1, sampler_2, sampler_3, sampler_4, sampler_5] | |
| schedulers = [scheduler_1, scheduler_2, scheduler_3, scheduler_4, scheduler_5] | |
| pairs = [] | |
| for sampler, scheduler in zip(samplers, schedulers): | |
| if sampler != "None": | |
| if scheduler != "None": | |
| pairs.append((sampler, scheduler)) | |
| else: | |
| pairs.append((sampler,None)) | |
| xy_type = "Sampler" | |
| xy_value = pairs | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: Scheduler Values | |
| class TSC_XYplot_Scheduler: | |
| schedulers = ["None"] + comfy.samplers.KSampler.SCHEDULERS | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "scheduler_1": (cls.schedulers,), | |
| "scheduler_2": (cls.schedulers,), | |
| "scheduler_3": (cls.schedulers,), | |
| "scheduler_4": (cls.schedulers,), | |
| "scheduler_5": (cls.schedulers,),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, scheduler_1, scheduler_2, scheduler_3, scheduler_4, scheduler_5): | |
| xy_type = "Scheduler" | |
| xy_value = [scheduler for scheduler in [scheduler_1, scheduler_2, scheduler_3, scheduler_4, scheduler_5] if | |
| scheduler != "None"] | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: Denoise Values | |
| class TSC_XYplot_Denoise: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "select_count": ("INT", {"default": 0, "min": 0, "max": 5}), | |
| "denoise_1": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), | |
| "denoise_2": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), | |
| "denoise_3": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), | |
| "denoise_4": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), | |
| "denoise_5": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, select_count, denoise_1, denoise_2, denoise_3, denoise_4, denoise_5): | |
| xy_type = "Denoise" | |
| xy_value = [denoise for idx, denoise in | |
| enumerate([denoise_1, denoise_2, denoise_3, denoise_4, denoise_5], start=1) if idx <= select_count] | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: VAE Values | |
| class TSC_XYplot_VAE: | |
| vaes = ["None"] + folder_paths.get_filename_list("vae") | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "vae_name_1": (cls.vaes,), | |
| "vae_name_2": (cls.vaes,), | |
| "vae_name_3": (cls.vaes,), | |
| "vae_name_4": (cls.vaes,), | |
| "vae_name_5": (cls.vaes,),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, vae_name_1, vae_name_2, vae_name_3, vae_name_4, vae_name_5): | |
| xy_type = "VAE" | |
| xy_value = [vae for vae in [vae_name_1, vae_name_2, vae_name_3, vae_name_4, vae_name_5] if vae != "None"] | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: Prompt S/R Positive | |
| class TSC_XYplot_PromptSR_Positive: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "search_txt": ("STRING", {"default": "", "multiline": False}), | |
| "replace_count": ("INT", {"default": 0, "min": 0, "max": 4}), | |
| "replace_1":("STRING", {"default": "", "multiline": False}), | |
| "replace_2": ("STRING", {"default": "", "multiline": False}), | |
| "replace_3": ("STRING", {"default": "", "multiline": False}), | |
| "replace_4": ("STRING", {"default": "", "multiline": False}),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, search_txt, replace_count, replace_1, replace_2, replace_3, replace_4): | |
| # If search_txt is empty, return (None,) | |
| if search_txt == "": | |
| return (None,) | |
| xy_type = "Positive Prompt S/R" | |
| # Create a list of replacement arguments | |
| replacements = [replace_1, replace_2, replace_3, replace_4] | |
| # Create base entry | |
| xy_values = [(search_txt, None)] | |
| if replace_count > 0: | |
| # Append additional entries based on replace_count | |
| xy_values.extend([(search_txt, replacements[i]) for i in range(replace_count)]) | |
| return ((xy_type, xy_values),) | |
| # TSC XY Plot: Prompt S/R Negative | |
| class TSC_XYplot_PromptSR_Negative: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "search_txt": ("STRING", {"default": "", "multiline": False}), | |
| "replace_count": ("INT", {"default": 0, "min": 0, "max": 4}), | |
| "replace_1":("STRING", {"default": "", "multiline": False}), | |
| "replace_2": ("STRING", {"default": "", "multiline": False}), | |
| "replace_3": ("STRING", {"default": "", "multiline": False}), | |
| "replace_4": ("STRING", {"default": "", "multiline": False}),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, search_txt, replace_count, replace_1, replace_2, replace_3, replace_4): | |
| # If search_txt is empty, return (None,) | |
| if search_txt == "": | |
| return (None,) | |
| xy_type = "Negative Prompt S/R" | |
| # Create a list of replacement arguments | |
| replacements = [replace_1, replace_2, replace_3, replace_4] | |
| # Create base entry | |
| xy_values = [(search_txt, None)] | |
| if replace_count > 0: | |
| # Append additional entries based on replace_count | |
| xy_values.extend([(search_txt, replacements[i]) for i in range(replace_count)]) | |
| return ((xy_type, xy_values),) | |
| # TSC XY Plot: Checkpoint Values | |
| class TSC_XYplot_Checkpoint: | |
| checkpoints = ["None"] + folder_paths.get_filename_list("checkpoints") | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "ckpt_name_1": (cls.checkpoints,), | |
| "clip_skip1": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}), | |
| "ckpt_name_2": (cls.checkpoints,), | |
| "clip_skip2": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}), | |
| "ckpt_name_3": (cls.checkpoints,), | |
| "clip_skip3": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}), | |
| "ckpt_name_4": (cls.checkpoints,), | |
| "clip_skip4": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}), | |
| "ckpt_name_5": (cls.checkpoints,), | |
| "clip_skip5": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, ckpt_name_1, clip_skip1, ckpt_name_2, clip_skip2, ckpt_name_3, clip_skip3, | |
| ckpt_name_4, clip_skip4, ckpt_name_5, clip_skip5): | |
| xy_type = "Checkpoint" | |
| checkpoints = [ckpt_name_1, ckpt_name_2, ckpt_name_3, ckpt_name_4, ckpt_name_5] | |
| clip_skips = [clip_skip1, clip_skip2, clip_skip3, clip_skip4, clip_skip5] | |
| xy_value = [(checkpoint, clip_skip) for checkpoint, clip_skip in zip(checkpoints, clip_skips) if | |
| checkpoint != "None"] | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: Clip Skip | |
| class TSC_XYplot_ClipSkip: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "select_count": ("INT", {"default": 0, "min": 0, "max": 5}), | |
| "clip_skip_1": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}), | |
| "clip_skip_2": ("INT", {"default": -2, "min": -24, "max": -1, "step": 1}), | |
| "clip_skip_3": ("INT", {"default": -3, "min": -24, "max": -1, "step": 1}), | |
| "clip_skip_4": ("INT", {"default": -4, "min": -24, "max": -1, "step": 1}), | |
| "clip_skip_5": ("INT", {"default": -5, "min": -24, "max": -1, "step": 1}),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, select_count, clip_skip_1, clip_skip_2, clip_skip_3, clip_skip_4, clip_skip_5): | |
| xy_type = "Clip Skip" | |
| xy_value = [clip_skip for idx, clip_skip in | |
| enumerate([clip_skip_1, clip_skip_2, clip_skip_3, clip_skip_4, clip_skip_5], start=1) if idx <= select_count] | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: LoRA Values | |
| class TSC_XYplot_LoRA: | |
| loras = ["None"] + folder_paths.get_filename_list("loras") | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "model_strengths": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "clip_strengths": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "lora_name_1": (cls.loras,), | |
| "lora_name_2": (cls.loras,), | |
| "lora_name_3": (cls.loras,), | |
| "lora_name_4": (cls.loras,), | |
| "lora_name_5": (cls.loras,)}, | |
| "optional": {"lora_stack": ("LORA_STACK", )} | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, model_strengths, clip_strengths, lora_name_1, lora_name_2, lora_name_3, lora_name_4, lora_name_5, | |
| lora_stack=None): | |
| xy_type = "LoRA" | |
| loras = [lora_name_1, lora_name_2, lora_name_3, lora_name_4, lora_name_5] | |
| # Extend each sub-array with lora_stack if it's not None | |
| xy_value = [[(lora, model_strengths, clip_strengths)] + (lora_stack if lora_stack else []) for lora in loras if | |
| lora != "None"] | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: LoRA Advanced | |
| class TSC_XYplot_LoRA_Adv: | |
| loras = ["None"] + folder_paths.get_filename_list("loras") | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "lora_name_1": (cls.loras,), | |
| "model_str_1": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "clip_str_1": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "lora_name_2": (cls.loras,), | |
| "model_str_2": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "clip_str_2": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "lora_name_3": (cls.loras,), | |
| "model_str_3": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "clip_str_3": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "lora_name_4": (cls.loras,), | |
| "model_str_4": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "clip_str_4": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "lora_name_5": (cls.loras,), | |
| "model_str_5": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), | |
| "clip_str_5": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),}, | |
| "optional": {"lora_stack": ("LORA_STACK",)} | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, lora_name_1, model_str_1, clip_str_1, lora_name_2, model_str_2, clip_str_2, lora_name_3, | |
| model_str_3, | |
| clip_str_3, lora_name_4, model_str_4, clip_str_4, lora_name_5, model_str_5, clip_str_5, | |
| lora_stack=None): | |
| xy_type = "LoRA" | |
| loras = [lora_name_1, lora_name_2, lora_name_3, lora_name_4, lora_name_5] | |
| model_strs = [model_str_1, model_str_2, model_str_3, model_str_4, model_str_5] | |
| clip_strs = [clip_str_1, clip_str_2, clip_str_3, clip_str_4, clip_str_5] | |
| # Extend each sub-array with lora_stack if it's not None | |
| xy_value = [[(lora, model_str, clip_str)] + (lora_stack if lora_stack else []) for lora, model_str, clip_str in | |
| zip(loras, model_strs, clip_strs) if lora != "None"] | |
| if not xy_value: # Check if the list is empty | |
| return (None,) | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: LoRA Stacks | |
| class TSC_XYplot_LoRA_Stacks: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "node_state": (["Enabled", "Disabled"],)}, | |
| "optional": { | |
| "lora_stack_1": ("LORA_STACK",), | |
| "lora_stack_2": ("LORA_STACK",), | |
| "lora_stack_3": ("LORA_STACK",), | |
| "lora_stack_4": ("LORA_STACK",), | |
| "lora_stack_5": ("LORA_STACK",),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, node_state, lora_stack_1=None, lora_stack_2=None, lora_stack_3=None, lora_stack_4=None, lora_stack_5=None): | |
| xy_type = "LoRA" | |
| xy_value = [stack for stack in [lora_stack_1, lora_stack_2, lora_stack_3, lora_stack_4, lora_stack_5] if stack is not None] | |
| if not xy_value or not any(xy_value) or node_state == "Disabled": | |
| return (None,) | |
| else: | |
| return ((xy_type, xy_value),) | |
| # TSC XY Plot: Manual Entry Notes | |
| class TSC_XYplot_Manual_XY_Entry_Info: | |
| syntax = "(X/Y_types) (X/Y_values)\n" \ | |
| "Seeds++ Batch batch_count\n" \ | |
| "Steps steps_1;steps_2;...\n" \ | |
| "CFG Scale cfg_1;cfg_2;...\n" \ | |
| "Sampler(1) sampler_1;sampler_2;...\n" \ | |
| "Sampler(2) sampler_1,scheduler_1;...\n" \ | |
| "Sampler(3) sampler_1;...;,default_scheduler\n" \ | |
| "Scheduler scheduler_1;scheduler_2;...\n" \ | |
| "Denoise denoise_1;denoise_2;...\n" \ | |
| "VAE vae_1;vae_2;vae_3;...\n" \ | |
| "+Prompt S/R search_txt;replace_1;replace_2;...\n" \ | |
| "-Prompt S/R search_txt;replace_1;replace_2;...\n" \ | |
| "Checkpoint(1) ckpt_1;ckpt_2;ckpt_3;...\n" \ | |
| "Checkpoint(2) ckpt_1,clip_skip_1;...\n" \ | |
| "Checkpoint(3) ckpt_1;ckpt_2;...;,default_clip_skip\n" \ | |
| "Clip Skip clip_skip_1;clip_skip_2;...\n" \ | |
| "LoRA(1) lora_1;lora_2;lora_3;...\n" \ | |
| "LoRA(2) lora_1;...;,default_model_str,default_clip_str\n" \ | |
| "LoRA(3) lora_1,model_str_1,clip_str_1;..." | |
| samplers = ";\n".join(comfy.samplers.KSampler.SAMPLERS) | |
| schedulers = ";\n".join(comfy.samplers.KSampler.SCHEDULERS) | |
| vaes = ";\n".join(folder_paths.get_filename_list("vae")) | |
| ckpts = ";\n".join(folder_paths.get_filename_list("checkpoints")) | |
| loras = ";\n".join(folder_paths.get_filename_list("loras")) | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "notes": ("STRING", {"default": | |
| f"_____________SYNTAX_____________\n{cls.syntax}\n\n" | |
| f"____________SAMPLERS____________\n{cls.samplers}\n\n" | |
| f"___________SCHEDULERS___________\n{cls.schedulers}\n\n" | |
| f"_____________VAES_______________\n{cls.vaes}\n\n" | |
| f"___________CHECKPOINTS__________\n{cls.ckpts}\n\n" | |
| f"_____________LORAS______________\n{cls.loras}\n","multiline": True}),},} | |
| RETURN_TYPES = () | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| # TSC XY Plot: Manual Entry | |
| class TSC_XYplot_Manual_XY_Entry: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "X_type": (["Nothing", "Seeds++ Batch", "Steps", "CFG Scale", "Sampler", "Scheduler", "Denoise", "VAE", | |
| "Positive Prompt S/R", "Negative Prompt S/R", "Checkpoint", "Clip Skip", "LoRA"],), | |
| "X_value": ("STRING", {"default": "", "multiline": True}), | |
| "Y_type": (["Nothing", "Seeds++ Batch", "Steps", "CFG Scale", "Sampler", "Scheduler", "Denoise", "VAE", | |
| "Positive Prompt S/R", "Negative Prompt S/R", "Checkpoint", "Clip Skip", "LoRA"],), | |
| "Y_value": ("STRING", {"default": "", "multiline": True}),},} | |
| RETURN_TYPES = ("XY", "XY",) | |
| RETURN_NAMES = ("X", "Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, X_type, X_value, Y_type, Y_value, prompt=None, my_unique_id=None): | |
| # Store X values as arrays | |
| if X_type not in {"Positive Prompt S/R", "Negative Prompt S/R", "VAE", "Checkpoint", "LoRA"}: | |
| X_value = X_value.replace(" ", "") # Remove spaces | |
| X_value = X_value.replace("\n", "") # Remove newline characters | |
| X_value = X_value.rstrip(";") # Remove trailing semicolon | |
| X_value = X_value.split(";") # Turn to array | |
| # Store Y values as arrays | |
| if Y_type not in {"Positive Prompt S/R", "Negative Prompt S/R", "VAE", "Checkpoint", "LoRA"}: | |
| Y_value = Y_value.replace(" ", "") # Remove spaces | |
| Y_value = Y_value.replace("\n", "") # Remove newline characters | |
| Y_value = Y_value.rstrip(";") # Remove trailing semicolon | |
| Y_value = Y_value.split(";") # Turn to array | |
| # Define the valid bounds for each type | |
| bounds = { | |
| "Seeds++ Batch": {"min": 0, "max": 50}, | |
| "Steps": {"min": 1, "max": 10000}, | |
| "CFG Scale": {"min": 0, "max": 100}, | |
| "Sampler": {"options": comfy.samplers.KSampler.SAMPLERS}, | |
| "Scheduler": {"options": comfy.samplers.KSampler.SCHEDULERS}, | |
| "Denoise": {"min": 0, "max": 1}, | |
| "VAE": {"options": folder_paths.get_filename_list("vae")}, | |
| "Checkpoint": {"options": folder_paths.get_filename_list("checkpoints")}, | |
| "Clip Skip": {"min": -24, "max": -1}, | |
| "LoRA": {"options": folder_paths.get_filename_list("loras"), | |
| "model_str": {"min": 0, "max": 10},"clip_str": {"min": 0, "max": 10},}, | |
| } | |
| # Validates a value based on its corresponding value_type and bounds. | |
| def validate_value(value, value_type, bounds): | |
| # ________________________________________________________________________ | |
| # Seeds++ Batch | |
| if value_type == "Seeds++ Batch": | |
| try: | |
| x = int(float(value)) | |
| if x < bounds["Seeds++ Batch"]["min"]: | |
| x = bounds["Seeds++ Batch"]["min"] | |
| elif x > bounds["Seeds++ Batch"]["max"]: | |
| x = bounds["Seeds++ Batch"]["max"] | |
| except ValueError: | |
| print(f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid batch count.") | |
| return None | |
| if float(value) != x: | |
| print(f"\033[31mmXY Plot Error:\033[0m '{value}' is not a valid batch count.") | |
| return None | |
| return x | |
| # ________________________________________________________________________ | |
| # Steps | |
| elif value_type == "Steps": | |
| try: | |
| x = int(value) | |
| if x < bounds["Steps"]["min"]: | |
| x = bounds["Steps"]["min"] | |
| elif x > bounds["Steps"]["max"]: | |
| x = bounds["Steps"]["max"] | |
| return x | |
| except ValueError: | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid Step count.") | |
| return None | |
| # ________________________________________________________________________ | |
| # CFG Scale | |
| elif value_type == "CFG Scale": | |
| try: | |
| x = float(value) | |
| if x < bounds["CFG Scale"]["min"]: | |
| x = bounds["CFG Scale"]["min"] | |
| elif x > bounds["CFG Scale"]["max"]: | |
| x = bounds["CFG Scale"]["max"] | |
| return x | |
| except ValueError: | |
| print( | |
| 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.") | |
| return None | |
| # ________________________________________________________________________ | |
| # Sampler | |
| elif value_type == "Sampler": | |
| if isinstance(value, str) and ',' in value: | |
| value = tuple(map(str.strip, value.split(','))) | |
| if isinstance(value, tuple): | |
| if len(value) >= 2: | |
| value = value[:2] # Slice the value tuple to keep only the first two elements | |
| sampler, scheduler = value | |
| scheduler = scheduler.lower() # Convert the scheduler name to lowercase | |
| if sampler not in bounds["Sampler"]["options"]: | |
| valid_samplers = '\n'.join(bounds["Sampler"]["options"]) | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{sampler}' is not a valid sampler. Valid samplers are:\n{valid_samplers}") | |
| sampler = None | |
| if scheduler not in bounds["Scheduler"]["options"]: | |
| valid_schedulers = '\n'.join(bounds["Scheduler"]["options"]) | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{scheduler}' is not a valid scheduler. Valid schedulers are:\n{valid_schedulers}") | |
| scheduler = None | |
| if sampler is None or scheduler is None: | |
| return None | |
| else: | |
| return sampler, scheduler | |
| else: | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid sampler.'") | |
| return None | |
| else: | |
| if value not in bounds["Sampler"]["options"]: | |
| valid_samplers = '\n'.join(bounds["Sampler"]["options"]) | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid sampler. Valid samplers are:\n{valid_samplers}") | |
| return None | |
| else: | |
| 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": | |
| try: | |
| x = float(value) | |
| if x < bounds["Denoise"]["min"]: | |
| x = bounds["Denoise"]["min"] | |
| elif x > bounds["Denoise"]["max"]: | |
| x = bounds["Denoise"]["max"] | |
| return x | |
| except ValueError: | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{value}' is not a number between {bounds['Denoise']['min']} " | |
| f"and {bounds['Denoise']['max']} for Denoise.") | |
| return None | |
| # ________________________________________________________________________ | |
| # VAE | |
| elif value_type == "VAE": | |
| if value not in bounds["VAE"]["options"]: | |
| valid_vaes = '\n'.join(bounds["VAE"]["options"]) | |
| print(f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid VAE. Valid VAEs are:\n{valid_vaes}") | |
| return None | |
| else: | |
| return value | |
| # ________________________________________________________________________ | |
| # Checkpoint | |
| elif value_type == "Checkpoint": | |
| if isinstance(value, str) and ',' in value: | |
| value = tuple(map(str.strip, value.split(','))) | |
| if isinstance(value, tuple): | |
| if len(value) >= 2: | |
| value = value[:2] # Slice the value tuple to keep only the first two elements | |
| checkpoint, clip_skip = value | |
| try: | |
| clip_skip = int(clip_skip) # Convert the clip_skip to integer | |
| except ValueError: | |
| print(f"\033[31mXY Plot Error:\033[0m '{clip_skip}' is not a valid clip_skip. " | |
| f"Valid clip skip values are integers between {bounds['Clip Skip']['min']} and {bounds['Clip Skip']['max']}.") | |
| return None | |
| if checkpoint not in bounds["Checkpoint"]["options"]: | |
| valid_checkpoints = '\n'.join(bounds["Checkpoint"]["options"]) | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{checkpoint}' is not a valid checkpoint. Valid checkpoints are:\n{valid_checkpoints}") | |
| checkpoint = None | |
| if clip_skip < bounds["Clip Skip"]["min"] or clip_skip > bounds["Clip Skip"]["max"]: | |
| print(f"\033[31mXY Plot Error:\033[0m '{clip_skip}' is not a valid clip skip. " | |
| f"Valid clip skip values are integers between {bounds['Clip Skip']['min']} and {bounds['Clip Skip']['max']}.") | |
| clip_skip = None | |
| if checkpoint is None or clip_skip is None: | |
| return None | |
| else: | |
| return checkpoint, clip_skip | |
| else: | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid checkpoint.'") | |
| return None | |
| else: | |
| if value not in bounds["Checkpoint"]["options"]: | |
| valid_checkpoints = '\n'.join(bounds["Checkpoint"]["options"]) | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid checkpoint. Valid checkpoints are:\n{valid_checkpoints}") | |
| return None | |
| else: | |
| return value, None | |
| # ________________________________________________________________________ | |
| # Clip Skip | |
| elif value_type == "Clip Skip": | |
| try: | |
| x = int(value) | |
| if x < bounds["Clip Skip"]["min"]: | |
| x = bounds["Clip Skip"]["min"] | |
| elif x > bounds["Clip Skip"]["max"]: | |
| x = bounds["Clip Skip"]["max"] | |
| return x | |
| except ValueError: | |
| print(f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid Clip Skip.") | |
| return None | |
| # ________________________________________________________________________ | |
| # LoRA | |
| elif value_type == "LoRA": | |
| if isinstance(value, str) and ',' in value: | |
| value = tuple(map(str.strip, value.split(','))) | |
| if isinstance(value, tuple): | |
| lora_name, model_str, clip_str = (value + (1.0, 1.0))[:3] # Defaults model_str and clip_str to 1 if not provided | |
| if lora_name not in bounds["LoRA"]["options"]: | |
| valid_loras = '\n'.join(bounds["LoRA"]["options"]) | |
| print(f"\033[31mXY Plot Error:\033[0m '{lora_name}' is not a valid LoRA. Valid LoRAs are:\n{valid_loras}") | |
| lora_name = None | |
| try: | |
| model_str = float(model_str) | |
| clip_str = float(clip_str) | |
| except ValueError: | |
| print(f"\033[31mXY Plot Error:\033[0m The LoRA model strength and clip strength values should be numbers" | |
| f" between {bounds['LoRA']['model_str']['min']} and {bounds['LoRA']['model_str']['max']}.") | |
| return None | |
| if model_str < bounds["LoRA"]["model_str"]["min"] or model_str > bounds["LoRA"]["model_str"]["max"]: | |
| print(f"\033[31mXY Plot Error:\033[0m '{model_str}' is not a valid LoRA model strength value. " | |
| f"Valid lora model strength values are between {bounds['LoRA']['model_str']['min']} and {bounds['LoRA']['model_str']['max']}.") | |
| model_str = None | |
| if clip_str < bounds["LoRA"]["clip_str"]["min"] or clip_str > bounds["LoRA"]["clip_str"]["max"]: | |
| print(f"\033[31mXY Plot Error:\033[0m '{clip_str}' is not a valid LoRA clip strength value. " | |
| f"Valid lora clip strength values are between {bounds['LoRA']['clip_str']['min']} and {bounds['LoRA']['clip_str']['max']}.") | |
| clip_str = None | |
| if lora_name is None or model_str is None or clip_str is None: | |
| return None | |
| else: | |
| return lora_name, model_str, clip_str | |
| else: | |
| if value not in bounds["LoRA"]["options"]: | |
| valid_loras = '\n'.join(bounds["LoRA"]["options"]) | |
| print( | |
| f"\033[31mXY Plot Error:\033[0m '{value}' is not a valid LoRA. Valid LoRAs are:\n{valid_loras}") | |
| return None | |
| else: | |
| return value, 1.0, 1.0 | |
| # ________________________________________________________________________ | |
| else: | |
| return None | |
| # Validate X_value array length is 1 if doing a "Seeds++ Batch" | |
| if len(X_value) != 1 and X_type == "Seeds++ Batch": | |
| print(f"\033[31mXY Plot Error:\033[0m '{';'.join(X_value)}' is not a valid batch count.") | |
| return (None,None,) | |
| # Validate Y_value array length is 1 if doing a "Seeds++ Batch" | |
| if len(Y_value) != 1 and Y_type == "Seeds++ Batch": | |
| print(f"\033[31mXY Plot Error:\033[0m '{';'.join(Y_value)}' is not a valid batch count.") | |
| return (None,None,) | |
| # Apply allowed shortcut syntax to certain input types | |
| if X_type in ["Sampler", "Checkpoint", "LoRA"]: | |
| if X_value[-1].startswith(','): | |
| # Remove the leading comma from the last entry and store it as suffixes | |
| suffixes = X_value.pop().lstrip(',').split(',') | |
| # Split all preceding entries into subentries | |
| X_value = [entry.split(',') for entry in X_value] | |
| # Make all entries the same length as suffixes by appending missing elements | |
| for entry in X_value: | |
| entry += suffixes[len(entry) - 1:] | |
| # Join subentries back into strings | |
| X_value = [','.join(entry) for entry in X_value] | |
| # Apply allowed shortcut syntax to certain input types | |
| if Y_type in ["Sampler", "Checkpoint", "LoRA"]: | |
| if Y_value[-1].startswith(','): | |
| # Remove the leading comma from the last entry and store it as suffixes | |
| suffixes = Y_value.pop().lstrip(',').split(',') | |
| # Split all preceding entries into subentries | |
| Y_value = [entry.split(',') for entry in Y_value] | |
| # Make all entries the same length as suffixes by appending missing elements | |
| for entry in Y_value: | |
| entry += suffixes[len(entry) - 1:] | |
| # Join subentries back into strings | |
| Y_value = [','.join(entry) for entry in Y_value] | |
| # Prompt S/R X Cleanup | |
| if X_type in {"Positive Prompt S/R", "Negative Prompt S/R"}: | |
| if X_value[0] == '': | |
| print(f"\033[31mXY Plot Error:\033[0m Prompt S/R value can not be empty.") | |
| return (None, None,) | |
| else: | |
| X_value = [(X_value[0], None) if i == 0 else (X_value[0], x) for i, x in enumerate(X_value)] | |
| # Prompt S/R X Cleanup | |
| if Y_type in {"Positive Prompt S/R", "Negative Prompt S/R"}: | |
| if Y_value[0] == '': | |
| print(f"\033[31mXY Plot Error:\033[0m Prompt S/R value can not be empty.") | |
| return (None, None,) | |
| else: | |
| Y_value = [(Y_value[0], None) if i == 0 else (Y_value[0], y) for i, y in enumerate(Y_value)] | |
| # Loop over each entry in X_value and check if it's valid | |
| if X_type not in {"Nothing", "Positive Prompt S/R", "Negative Prompt S/R"}: | |
| for i in range(len(X_value)): | |
| X_value[i] = validate_value(X_value[i], X_type, bounds) | |
| if X_value[i] == None: | |
| return (None,None,) | |
| # Loop over each entry in Y_value and check if it's valid | |
| if Y_type not in {"Nothing", "Positive Prompt S/R", "Negative Prompt S/R"}: | |
| for i in range(len(Y_value)): | |
| Y_value[i] = validate_value(Y_value[i], Y_type, bounds) | |
| if Y_value[i] == None: | |
| return (None,None,) | |
| # Nest LoRA value in another array to reflect LoRA stack changes | |
| if X_type == "LoRA": | |
| X_value = [X_value] | |
| if Y_type == "LoRA": | |
| Y_value = [Y_value] | |
| # Clean X/Y_values | |
| if X_type == "Nothing": | |
| X_value = [""] | |
| if Y_type == "Nothing": | |
| Y_value = [""] | |
| return ((X_type, X_value), (Y_type, Y_value),) | |
| # TSC XY Plot: Seeds Values | |
| class TSC_XYplot_JoinInputs: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "XY_1": ("XY",), | |
| "XY_2": ("XY",),}, | |
| } | |
| RETURN_TYPES = ("XY",) | |
| RETURN_NAMES = ("X or Y",) | |
| FUNCTION = "xy_value" | |
| CATEGORY = "Efficiency Nodes/XY Plot/XY Inputs" | |
| def xy_value(self, XY_1, XY_2): | |
| xy_type_1, xy_value_1 = XY_1 | |
| xy_type_2, xy_value_2 = XY_2 | |
| if xy_type_1 != xy_type_2: | |
| print(f"\033[31mJoin XY Inputs Error:\033[0m Input types must match") | |
| return (None,) | |
| elif xy_type_1 == "Seeds++ Batch": | |
| xy_type = xy_type_1 | |
| xy_value = [xy_value_1[0] + xy_value_2[0]] | |
| elif xy_type_1 == "Positive Prompt S/R" or xy_type_1 == "Negative Prompt S/R": | |
| xy_type = xy_type_1 | |
| xy_value = xy_value_1 + [(xy_value_1[0][0], t[1]) for t in xy_value_2[1:]] | |
| else: | |
| xy_type = xy_type_1 | |
| xy_value = xy_value_1 + xy_value_2 | |
| return ((xy_type, xy_value),) | |
| ######################################################################################################################## | |
| # TSC Image Overlay | |
| class TSC_ImageOverlay: | |
| def INPUT_TYPES(cls): | |
| return { | |
| "required": { | |
| "base_image": ("IMAGE",), | |
| "overlay_image": ("IMAGE",), | |
| "overlay_resize": (["None", "Fit", "Resize by rescale_factor", "Resize to width & heigth"],), | |
| "resize_method": (["nearest-exact", "bilinear", "area"],), | |
| "rescale_factor": ("FLOAT", {"default": 1, "min": 0.01, "max": 16.0, "step": 0.1}), | |
| "width": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 64}), | |
| "height": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 64}), | |
| "x_offset": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 10}), | |
| "y_offset": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 10}), | |
| "rotation": ("INT", {"default": 0, "min": -180, "max": 180, "step": 5}), | |
| "opacity": ("FLOAT", {"default": 0, "min": 0, "max": 100, "step": 5}), | |
| }, | |
| "optional": {"optional_mask": ("MASK",),} | |
| } | |
| RETURN_TYPES = ("IMAGE",) | |
| FUNCTION = "apply_overlay_image" | |
| CATEGORY = "Efficiency Nodes/Image" | |
| def apply_overlay_image(self, base_image, overlay_image, overlay_resize, resize_method, rescale_factor, | |
| width, height, x_offset, y_offset, rotation, opacity, optional_mask=None): | |
| # Pack tuples and assign variables | |
| size = width, height | |
| location = x_offset, y_offset | |
| mask = optional_mask | |
| # Check for different sizing options | |
| if overlay_resize != "None": | |
| #Extract overlay_image size and store in Tuple "overlay_image_size" (WxH) | |
| overlay_image_size = overlay_image.size() | |
| overlay_image_size = (overlay_image_size[2], overlay_image_size[1]) | |
| if overlay_resize == "Fit": | |
| overlay_image_size = (base_image.size[0],base_image.size[1]) | |
| elif overlay_resize == "Resize by rescale_factor": | |
| overlay_image_size = tuple(int(dimension * rescale_factor) for dimension in overlay_image_size) | |
| elif overlay_resize == "Resize to width & heigth": | |
| overlay_image_size = (size[0], size[1]) | |
| samples = overlay_image.movedim(-1, 1) | |
| overlay_image = comfy.utils.common_upscale(samples, overlay_image_size[0], overlay_image_size[1], resize_method, False) | |
| overlay_image = overlay_image.movedim(1, -1) | |
| overlay_image = tensor2pil(overlay_image) | |
| # Add Alpha channel to overlay | |
| overlay_image = overlay_image.convert('RGBA') | |
| overlay_image.putalpha(Image.new("L", overlay_image.size, 255)) | |
| # If mask connected, check if the overlay_image image has an alpha channel | |
| if mask is not None: | |
| # Convert mask to pil and resize | |
| mask = tensor2pil(mask) | |
| mask = mask.resize(overlay_image.size) | |
| # Apply mask as overlay's alpha | |
| overlay_image.putalpha(ImageOps.invert(mask)) | |
| # Rotate the overlay image | |
| overlay_image = overlay_image.rotate(rotation, expand=True) | |
| # Apply opacity on overlay image | |
| r, g, b, a = overlay_image.split() | |
| a = a.point(lambda x: max(0, int(x * (1 - opacity / 100)))) | |
| overlay_image.putalpha(a) | |
| # Split the base_image tensor along the first dimension to get a list of tensors | |
| base_image_list = torch.unbind(base_image, dim=0) | |
| # Convert each tensor to a PIL image, apply the overlay, and then convert it back to a tensor | |
| processed_base_image_list = [] | |
| for tensor in base_image_list: | |
| # Convert tensor to PIL Image | |
| image = tensor2pil(tensor) | |
| # Paste the overlay image onto the base image | |
| if mask is None: | |
| image.paste(overlay_image, location) | |
| else: | |
| image.paste(overlay_image, location, overlay_image) | |
| # Convert PIL Image back to tensor | |
| processed_tensor = pil2tensor(image) | |
| # Append to list | |
| processed_base_image_list.append(processed_tensor) | |
| # Combine the processed images back into a single tensor | |
| base_image = torch.stack([tensor.squeeze() for tensor in processed_base_image_list]) | |
| # Return the edited base image | |
| return (base_image,) | |
| ######################################################################################################################## | |
| # Install simple_eval if missing from packages | |
| def install_simpleeval(): | |
| if 'simpleeval' not in packages(): | |
| print("\033[32mEfficiency Nodes:\033[0m") | |
| subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'simpleeval']) | |
| def packages(versions=False): | |
| return [(r.decode().split('==')[0] if not versions else r.decode()) for r in subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']).split()] | |
| install_simpleeval() | |
| from simpleeval import simple_eval | |
| # TSC Evaluate Integers (https://github.com/danthedeckie/simpleeval) | |
| class TSC_EvaluateInts: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "python_expression": ("STRING", {"default": "((a + b) - c) / 2", "multiline": False}), | |
| "print_to_console": (["False", "True"],),}, | |
| "optional": { | |
| "a": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}), | |
| "b": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}), | |
| "c": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}),}, | |
| } | |
| RETURN_TYPES = ("INT", "FLOAT", "STRING",) | |
| OUTPUT_NODE = True | |
| FUNCTION = "evaluate" | |
| CATEGORY = "Efficiency Nodes/Simple Eval" | |
| def evaluate(self, python_expression, print_to_console, a=0, b=0, c=0): | |
| # simple_eval doesn't require the result to be converted to a string | |
| result = simple_eval(python_expression, names={'a': a, 'b': b, 'c': c}) | |
| int_result = int(result) | |
| float_result = float(result) | |
| string_result = str(result) | |
| if print_to_console == "True": | |
| print("\n\033[31mEvaluate Integers:\033[0m") | |
| print(f"\033[90m{{a = {a} , b = {b} , c = {c}}} \033[0m") | |
| print(f"{python_expression} = \033[92m INT: " + str(int_result) + " , FLOAT: " + str( | |
| float_result) + ", STRING: " + string_result + "\033[0m") | |
| return (int_result, float_result, string_result,) | |
| # TSC Evaluate Floats (https://github.com/danthedeckie/simpleeval) | |
| class TSC_EvaluateFloats: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "python_expression": ("STRING", {"default": "((a + b) - c) / 2", "multiline": False}), | |
| "print_to_console": (["False", "True"],),}, | |
| "optional": { | |
| "a": ("FLOAT", {"default": 0, "min": -sys.float_info.max, "max": sys.float_info.max, "step": 1}), | |
| "b": ("FLOAT", {"default": 0, "min": -sys.float_info.max, "max": sys.float_info.max, "step": 1}), | |
| "c": ("FLOAT", {"default": 0, "min": -sys.float_info.max, "max": sys.float_info.max, "step": 1}),}, | |
| } | |
| RETURN_TYPES = ("INT", "FLOAT", "STRING",) | |
| OUTPUT_NODE = True | |
| FUNCTION = "evaluate" | |
| CATEGORY = "Efficiency Nodes/Simple Eval" | |
| def evaluate(self, python_expression, print_to_console, a=0, b=0, c=0): | |
| # simple_eval doesn't require the result to be converted to a string | |
| result = simple_eval(python_expression, names={'a': a, 'b': b, 'c': c}) | |
| int_result = int(result) | |
| float_result = float(result) | |
| string_result = str(result) | |
| if print_to_console == "True": | |
| print("\n\033[31mEvaluate Floats:\033[0m") | |
| print(f"\033[90m{{a = {a} , b = {b} , c = {c}}} \033[0m") | |
| print(f"{python_expression} = \033[92m INT: " + str(int_result) + " , FLOAT: " + str( | |
| float_result) + ", STRING: " + string_result + "\033[0m") | |
| return (int_result, float_result, string_result,) | |
| # TSC Evaluate Strings (https://github.com/danthedeckie/simpleeval) | |
| class TSC_EvaluateStrs: | |
| def INPUT_TYPES(cls): | |
| return {"required": { | |
| "python_expression": ("STRING", {"default": "a + b + c", "multiline": False}), | |
| "print_to_console": (["False", "True"],)}, | |
| "optional": { | |
| "a": ("STRING", {"default": "Hello", "multiline": False}), | |
| "b": ("STRING", {"default": " World", "multiline": False}), | |
| "c": ("STRING", {"default": "!", "multiline": False}),} | |
| } | |
| RETURN_TYPES = ("STRING",) | |
| OUTPUT_NODE = True | |
| FUNCTION = "evaluate" | |
| CATEGORY = "Efficiency Nodes/Simple Eval" | |
| def evaluate(self, python_expression, print_to_console, a="", b="", c=""): | |
| variables = {'a': a, 'b': b, 'c': c} # Define the variables for the expression | |
| functions = {"len": len} # Define the functions for the expression | |
| result = simple_eval(python_expression, names=variables, functions=functions) | |
| if print_to_console == "True": | |
| print("\n\033[31mEvaluate Strings:\033[0m") | |
| print(f"\033[90ma = {a} \nb = {b} \nc = {c}\033[0m") | |
| print(f"{python_expression} = \033[92m" + str(result) + "\033[0m") | |
| return (str(result),) # Convert result to a string before returning | |
| # TSC Simple Eval Examples (https://github.com/danthedeckie/simpleeval) | |
| class TSC_EvalExamples: | |
| filepath = os.path.join(my_dir, 'workflows', 'SimpleEval_Node_Examples.txt') | |
| with open(filepath, 'r') as file: | |
| examples = file.read() | |
| def INPUT_TYPES(cls): | |
| return {"required": { "models_text": ("STRING", {"default": cls.examples ,"multiline": True}),},} | |
| RETURN_TYPES = () | |
| CATEGORY = "Efficiency Nodes/Simple Eval" | |
| # NODE MAPPING | |
| NODE_CLASS_MAPPINGS = { | |
| "KSampler (Efficient)": TSC_KSampler, | |
| "Efficient Loader": TSC_EfficientLoader, | |
| "LoRA Stacker": TSC_LoRA_Stacker, | |
| "LoRA Stacker Adv.": TSC_LoRA_Stacker_Adv, | |
| "XY Plot": TSC_XYplot, | |
| "XY Input: Seeds++ Batch": TSC_XYplot_SeedsBatch, | |
| "XY Input: Steps": TSC_XYplot_Steps, | |
| "XY Input: CFG Scale": TSC_XYplot_CFG, | |
| "XY Input: Sampler": TSC_XYplot_Sampler, | |
| "XY Input: Scheduler": TSC_XYplot_Scheduler, | |
| "XY Input: Denoise": TSC_XYplot_Denoise, | |
| "XY Input: VAE": TSC_XYplot_VAE, | |
| "XY Input: Positive Prompt S/R": TSC_XYplot_PromptSR_Positive, | |
| "XY Input: Negative Prompt S/R": TSC_XYplot_PromptSR_Negative, | |
| "XY Input: Checkpoint": TSC_XYplot_Checkpoint, | |
| "XY Input: Clip Skip": TSC_XYplot_ClipSkip, | |
| "XY Input: LoRA": TSC_XYplot_LoRA, | |
| "XY Input: LoRA Adv.": TSC_XYplot_LoRA_Adv, | |
| "XY Input: LoRA Stacks": TSC_XYplot_LoRA_Stacks, | |
| "XY Input: Manual XY Entry": TSC_XYplot_Manual_XY_Entry, | |
| "Manual XY Entry Info": TSC_XYplot_Manual_XY_Entry_Info, | |
| "Join XY Inputs of Same Type": TSC_XYplot_JoinInputs, | |
| "Image Overlay": TSC_ImageOverlay, | |
| "Evaluate Integers": TSC_EvaluateInts, | |
| "Evaluate Floats": TSC_EvaluateFloats, | |
| "Evaluate Strings": TSC_EvaluateStrs, | |
| "Simple Eval Examples": TSC_EvalExamples | |
| } |