From ec45e870976b5f242cd4764036ac85579fef388b Mon Sep 17 00:00:00 2001 From: TSC <112517630+LucianoCirino@users.noreply.github.com> Date: Sun, 11 Jun 2023 18:01:40 -0500 Subject: [PATCH] V1.57 --- efficiency_nodes.py | 966 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 828 insertions(+), 138 deletions(-) diff --git a/efficiency_nodes.py b/efficiency_nodes.py index c76f7dc..379a96b 100644 --- a/efficiency_nodes.py +++ b/efficiency_nodes.py @@ -81,7 +81,7 @@ loaded_objects = { } def print_loaded_objects_entries(id=None, prompt=None, show_id=False): - print("\n" + "-" * 40) # Print an empty line followed by a separator line + print("-" * 40) # Print an empty line followed by a separator line if id is not None: id = str(id) # Convert ID to string if prompt is not None and id is not None: @@ -94,29 +94,33 @@ def print_loaded_objects_entries(id=None, prompt=None, show_id=False): print(f"\033[36mGlobal Models Cache:\033[0m") else: print(f"\033[36mModels Cache: \nnode_id:{int(id)}\033[0m") - print("- " * 20) # Print an empty line followed by a separator line + entries_found = False for key in ["ckpt", "vae", "lora"]: entries_with_id = loaded_objects[key] if id is None else [entry for entry in loaded_objects[key] if id in entry[-1]] if not entries_with_id: # If no entries with the chosen ID, print None and skip this key continue + entries_found = True print(f"{key.capitalize()}:") for i, entry in enumerate(entries_with_id, 1): # Start numbering from 1 truncated_name = entry[0][:50] # Truncate at 50 characters if key == "lora": - lora_weight_rounded = round(entry[4], 3) # Round lora_weight to 3 decimal places + lora_model_str_rounded = round(entry[4], 2) # Round lora_weight to 2 decimal places + lora_clip_str_rounded = round(entry[4], 2) # Round lora_weight to 2 decimal places if id is None: associated_ids = ', '.join(map(str, entry[-1])) # Gather all associated ids - print(f" [{i}] {truncated_name} (ids: {associated_ids}, lora_weight: {lora_weight_rounded}, ckpt_name: {entry[1]})") + print(f" [{i}] {truncated_name} (ids: {associated_ids}, {lora_model_str_rounded}," + f" {lora_clip_str_rounded}, base_ckpt: {entry[1]})") else: - print(f" [{i}] {truncated_name} (lora_weight: {lora_weight_rounded}, base_ckpt: {entry[1]})") + print(f" [{i}] {truncated_name} ({lora_model_str_rounded}," + f" {lora_clip_str_rounded}, base_ckpt: {entry[1]})") else: if id is None: associated_ids = ', '.join(map(str, entry[-1])) # Gather all associated ids print(f" [{i}] {truncated_name} (ids: {associated_ids})") else: print(f" [{i}] {truncated_name}") - #print("-" * 40) # Print a separator line - #print("\n") # Print an empty line + if not entries_found: + print("-") # This function cleans global variables associated with nodes that are no longer detected on UI def globals_cleanup(prompt): @@ -144,7 +148,7 @@ def globals_cleanup(prompt): loaded_objects[key].remove(tup) ###print(f'Deleted tuple at index {i} in {key} in loaded_objects because its id array became empty.') -def load_checkpoint(ckpt_name, id, output_vae=True, cache=None): +def load_checkpoint(ckpt_name, id, output_vae=True, cache=None, cache_overwrite=False): """ Searches for tuple index that contains ckpt_name in "ckpt" array of loaded_objects. If found, extracts the model, clip, and vae from the loaded_objects. @@ -186,10 +190,26 @@ def load_checkpoint(ckpt_name, id, output_vae=True, cache=None): loaded_objects["ckpt"].append((ckpt_name, model, clip, vae, [id])) else: clear_cache(id, cache, "ckpt") + if cache_overwrite: + # Find the first entry with the id, remove the id from the entry's id list + for e in loaded_objects["ckpt"]: + if id in e[-1]: + e[-1].remove(id) + # If the id list becomes empty, remove the entry from the "ckpt" list + if not e[-1]: + loaded_objects["ckpt"].remove(e) + break + loaded_objects["ckpt"].append((ckpt_name, model, clip, vae, [id])) return model, clip, vae -def load_vae(vae_name, id, cache=None): +def get_bvae_by_ckpt_name(ckpt_name): + for ckpt in loaded_objects["ckpt"]: + if ckpt[0] == ckpt_name: + return ckpt[3] # return 'bvae' variable + return None # return None if no match is found + +def load_vae(vae_name, id, cache=None, cache_overwrite=False): """ Extracts the vae with a given name from the "vae" array in loaded_objects. If the vae is not found, creates a new VAE object with the given name and adds it to the "vae" array. @@ -224,10 +244,20 @@ def load_vae(vae_name, id, cache=None): loaded_objects["vae"].append((vae_name, vae, [id])) else: clear_cache(id, cache, "vae") + if cache_overwrite: + # Find the first entry with the id, remove the id from the entry's id list + for e in loaded_objects["vae"]: + if id in e[-1]: + e[-1].remove(id) + # If the id list becomes empty, remove the entry from the "vae" list + if not e[-1]: + loaded_objects["vae"].remove(e) + break + loaded_objects["vae"].append((vae_name, vae, [id])) return vae -def load_lora(lora_name, ckpt_name, strength_model, strength_clip, id, cache=None): +def load_lora(lora_name, ckpt_name, strength_model, strength_clip, id, cache=None, ckpt_cache=None, cache_overwrite=False): """ Extracts the Lora model with a given name from the "lora" array in loaded_objects. If the Lora model is not found or strength values changed or model changed, creates a new Lora object with the given name and adds it to the "lora" array. @@ -258,7 +288,7 @@ def load_lora(lora_name, ckpt_name, strength_model, strength_clip, id, cache=Non return lora_model, lora_clip - ckpt, clip, _ = load_checkpoint(ckpt_name, id, output_vae=False, cache=None) + ckpt, clip, _ = load_checkpoint(ckpt_name, id, cache=ckpt_cache, cache_overwrite=cache_overwrite) lora_path = folder_paths.get_full_path("loras", lora_name) lora_model, lora_clip = comfy.sd.load_lora_for_models(ckpt, clip, lora_path, strength_model, strength_clip) @@ -267,10 +297,19 @@ def load_lora(lora_name, ckpt_name, strength_model, strength_clip, id, cache=Non loaded_objects["lora"].append((lora_name, ckpt_name, lora_model, lora_clip, strength_model, strength_clip, [id])) else: clear_cache(id, cache, "lora") + if cache_overwrite: + # Find the first entry with the id, remove the id from the entry's id list + for e in loaded_objects["lora"]: + if id in e[-1]: + e[-1].remove(id) + # If the id list becomes empty, remove the entry from the "lora" list + if not e[-1]: + loaded_objects["lora"].remove(e) + break + loaded_objects["lora"].append((lora_name, ckpt_name, lora_model, lora_clip, strength_model, strength_clip, [id])) return lora_model, lora_clip - def clear_cache(id, cache, dict_name): """ Clear the cache for a specific id in a specific dictionary (either "ckpt" or "vae"). @@ -339,20 +378,20 @@ def clear_cache_by_exception(node_id, vae_dict=None, ckpt_dict=None, lora_dict=N if not tuple_item[-1]: loaded_objects[dict_name].remove(tuple_item) -# Retrieve the cache number from 'cache_settings' json file +# Retrieve the cache number from 'node_settings' json file def get_cache_numbers(node_name): # Get the directory path of the current file my_dir = os.path.dirname(os.path.abspath(__file__)) - # Construct the file path for cache_settings.json - settings_file = os.path.join(my_dir, 'cache_settings.json') + # 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: - cache_settings = json.load(file) + node_settings = json.load(file) # Retrieve the cache numbers for the given node - cache_numbers = cache_settings.get(node_name, {}) - vae_cache = int(cache_numbers.get('vae', 1)) - ckpt_cache = int(cache_numbers.get('ckpt', 1)) - lora_cache = int(cache_numbers.get('lora', 1)) + model_cache_settings = node_settings.get(node_name, {}).get('model_cache', {}) + vae_cache = int(model_cache_settings.get('vae', 1)) + ckpt_cache = int(model_cache_settings.get('ckpt', 1)) + lora_cache = int(model_cache_settings.get('lora', 1)) return vae_cache, ckpt_cache, lora_cache ######################################################################################################################## @@ -398,15 +437,18 @@ class TSC_EfficientLoader: # Retrieve cache numbers vae_cache, ckpt_cache, lora_cache = get_cache_numbers("Efficient Loader") - # Load models - model, clip, vae = load_checkpoint(ckpt_name, my_unique_id, cache=ckpt_cache) - if lora_name != "None": - model, clip = load_lora(lora_name, ckpt_name, lora_model_strength, lora_clip_strength,my_unique_id, cache=lora_cache) + model, clip = load_lora(lora_name, ckpt_name, lora_model_strength, lora_clip_strength, 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_name = None # Check for custom VAE if vae_name != "Baked VAE": - vae = load_vae(vae_name, my_unique_id, cache=vae_cache) + vae = load_vae(vae_name, my_unique_id, cache=vae_cache, cache_overwrite=True) # CLIP skip if not clip: @@ -415,7 +457,7 @@ class TSC_EfficientLoader: clip.clip_layer(clip_skip) # Data for XY Plot - dependencies = (vae_name, ckpt_name, clip, clip_skip, positive, negative) + dependencies = (vae_name, ckpt_name, clip, clip_skip, positive, negative, lora_name, lora_model_strength, lora_clip_strength) return (model, [[clip.encode(positive), {}]], [[clip.encode(negative), {}]], {"samples":latent}, vae, dependencies, ) @@ -435,7 +477,6 @@ def print_last_helds(id=None): print(f"Node-specific Last Helds (node_id:{int(id)})") else: print(f"Global Last Helds:") - print("- " * 20) # Print an empty line followed by a separator line for key in ["results", "latent", "images", "vae_decode"]: entries_with_id = last_helds[key] if id is None else [entry for entry in last_helds[key] if id == entry[-1]] if not entries_with_id: # If no entries with the chosen ID, print None and skip this key @@ -492,6 +533,23 @@ class TSC_KSampler: 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 + xyplot_as_output_image = node_settings.get("KSampler (Efficient)", {}).get('xyplot_as_output_image', False) + 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 xyplot_as_output_image, kse_vae_tiled, xy_vae_tiled + + xyplot_as_output_image, 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)) @@ -624,7 +682,10 @@ class TSC_KSampler: "result": (model, positive, negative, {"samples": latent}, vae, TSC_KSampler.empty_image,)} else: # Decode images and store - images = vae.decode(latent).cpu() + 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 @@ -642,27 +703,21 @@ class TSC_KSampler: # If the sampler state is "Hold" elif sampler_state == "Hold": - #Debug - ###print_last_helds() - ###print_loaded_objects_entries() - ###print("\n" + "-" * 40) # Print an empty line followed by a separator line - - # Print a message indicating that the KSampler is in "Hold" state with the unique ID - print('\033[32mKSampler(Efficient)[{}]:\033[0mHeld'.format(my_unique_id)) - # 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,)} - # if preview_image == "Enabled": else: latent = last_latent["samples"] if get_value_by_id("vae_decode", my_unique_id) == True: # Decode images and store - images = vae.decode(latent).cpu() + 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 @@ -687,14 +742,15 @@ class TSC_KSampler: 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"): - print('\033[31mKSampler(Efficient)[{}] Error:\033[0m No valid script input detected'.format(my_unique_id)) + 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.'.format(my_unique_id)) + 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,)} @@ -730,7 +786,30 @@ class TSC_KSampler: # Unpack Effficient Loader dependencies if dependencies is not None: - vae_name, ckpt_name, clip, clip_skip, positive_prompt, negative_prompt = dependencies + vae_name, ckpt_name, clip, clip_skip, positive_prompt, negative_prompt,\ + lora_name, lora_model_wt, lora_clip_wt = dependencies + + # Helper function to process printout values + def process_xy_for_print(value, replacement): + if 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) for v in X_value] + Y_value_processed = [process_xy_for_print(v, replacement_Y) 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": @@ -741,6 +820,10 @@ class TSC_KSampler: # 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) + # Define X/Y_values for "Seeds++ Batch" if X_type == "Seeds++ Batch": X_value = [i for i in range(X_value[0])] @@ -763,10 +846,13 @@ class TSC_KSampler: else: Y_value[i] = [Y_value[i], None] - # Optimize image generation by prioritizing Checkpoint>LoRA>VAE as X in For Loop. Flip back when done. - if Y_type == "Checkpoint" or ( Y_type == "LoRA" and X_type != "Checkpoint") or \ - (Y_type == "VAE" and (X_type != "Checkpoint" and X_type != "LoRA")) or \ - (X_type == "Nothing" and Y_type != "Nothing"): + # 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 @@ -795,7 +881,6 @@ class TSC_KSampler: if ckpt_dict and lora_dict: lora_dict = [(lora_name, ckpt, lora_model_wt, lora_clip_wt) for ckpt in ckpt_dict for lora_name, lora_model_wt, lora_clip_wt 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_name, ckpt_name, lora_model_wt, lora_clip_wt) for @@ -807,7 +892,7 @@ class TSC_KSampler: # prioritize Caching Checkpoints over LoRAs but not both. if X_type == "LoRA": ckpt_dict = [] - if Y_type == "LoRA": # This implies X_type == "Checkpoint" + if X_type == "Checkpoint": lora_dict = [] # Print dict_arrays for debugging @@ -815,11 +900,12 @@ class TSC_KSampler: # 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) - + #_______________________________________________________________________________________________________ # 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, lora_name, lora_model_wt, lora_clip_wt, var_label, num_label): + def define_variable(var_type, var, seed, steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, + clip_skip, positive_prompt, negative_prompt, lora_name, lora_model_wt, lora_clip_wt, + var_label, num_label): # If var_type is "Seeds++ Batch", update var and seed, and generate labels if var_type == "Seeds++ Batch": @@ -842,17 +928,17 @@ class TSC_KSampler: text = f"{sampler_name}" else: if var[1] != None: - scheduler[0] = var[1] + scheduler = (var[1], scheduler[1]) else: - scheduler[0] = scheduler[1] + scheduler = (scheduler[1], scheduler[1]) text = f"{sampler_name} ({scheduler[0]})" text = text.replace("ancestral", "a").replace("uniform", "u") # If var_type is "Scheduler", update scheduler and generate labels elif var_type == "Scheduler": - scheduler[0] = var[0] + scheduler = (var[0], scheduler[1]) if len(var) == 2: - text = f"{sampler_name} ({var[0]})" + text = f"{sampler_name} ({scheduler[0]})" else: text = f"{var}" text = text.replace("ancestral", "a").replace("uniform", "u") @@ -868,14 +954,41 @@ class TSC_KSampler: vae_filename = os.path.basename(vae_name) 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] - clip_skip = var[1] + if var[1] == None: + clip_skip = (clip_skip[1],clip_skip[1]) + else: + clip_skip = (var[1],clip_skip[1]) ckpt_filename = os.path.basename(ckpt_name) text = f"{ckpt_filename}" - #text = f"{ckpt_filename[:16]}... ({clip_skip})" if len( - #ckpt_filename) > 16 else f"{ckpt_filename} ({clip_skip})" + #text = f"{ckpt_filename[:16]}... ({clip_skip[0]})" if len( + #ckpt_filename) > 16 else f"{ckpt_filename} ({clip_skip[0]})" + + elif var_type == "Clip Skip": + clip_skip = (var, clip_skip[1]) + text = f"Clip Skip ({clip_skip[0]})" # If var_type is "LoRA", update lora_model and lora_clip (if needed) and generate labels elif var_type == "LoRA": @@ -883,7 +996,10 @@ class TSC_KSampler: lora_model_wt = var[1] lora_clip_wt = var[2] lora_filename = os.path.basename(lora_name) - text = f" {lora_filename}" + if lora_model_wt == lora_clip_wt: + text = f"<{round(lora_model_wt, 2)}> {lora_filename}" + else: + text = f"<{round(lora_model_wt, 2)},{round(lora_clip_wt, 2)}> {lora_filename}" # For any other var_type, set text to "" else: @@ -913,8 +1029,8 @@ class TSC_KSampler: var_label = truncate_texts(var_label, num_label) # Return the modified variables - return steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip,\ - lora_name, lora_model_wt, lora_clip_wt, var_label + return steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip, \ + positive_prompt, negative_prompt, lora_name, lora_model_wt, lora_clip_wt, var_label # _______________________________________________________________________________________________________ # The function below is used to smartly load Checkpoint/LoRA/VAE models between generations. @@ -932,23 +1048,40 @@ class TSC_KSampler: # 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"): - model, clip, _ = load_checkpoint(ckpt_name, script_node_id, False, cache=cache[1]) + if lora_name is None: + model, clip, _ = load_checkpoint(ckpt_name, script_node_id, False, cache=cache[1]) + else: # Load Efficient Loader LoRA + model, clip = load_lora(lora_name, ckpt_name, lora_model_wt, lora_clip_wt, script_node_id, + cache=None, ckpt_cache=cache[1]) encode = True # Load LoRA if required - if (X_type == "LoRA" and index == 0) or Y_type == "LoRA": + elif (X_type == "LoRA" and index == 0): + # Don't cache Checkpoints model, clip = load_lora(lora_name, ckpt_name, lora_model_wt, lora_clip_wt, 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_name, ckpt_name, lora_model_wt, lora_clip_wt, 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, negative_prompt, clip, clip_skip) + positive, negative = encode_prompt(positive_prompt[0], negative_prompt[0], clip, clip_skip) return model, positive, negative, vae @@ -968,7 +1101,10 @@ class TSC_KSampler: latent_list.append(latent) # Decode the latent tensor - image = vae.decode(latent).cpu() + 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) @@ -990,7 +1126,10 @@ class TSC_KSampler: seed_updated = seed # Store the KSamplers original scheduler inside the same scheduler variable - scheduler = [scheduler, scheduler] + 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) @@ -1004,16 +1143,17 @@ class TSC_KSampler: seed_updated = seed + X_index # Define X parameters and generate labels - steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip,\ + steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip, positive_prompt, negative_prompt, \ lora_name, lora_model_wt, lora_clip_wt, X_label = \ - define_variable(X_type, X, seed_updated, steps, cfg, sampler_name, scheduler, denoise, vae_name, - ckpt_name, clip_skip, lora_name, lora_model_wt, lora_clip_wt, X_label, len(X_value)) + define_variable(X_type, X, seed_updated, steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, + clip_skip, positive_prompt, negative_prompt, lora_name, lora_model_wt, lora_clip_wt, + 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, vae, + define_model(model, clip, positive, negative, positive_prompt, negative_prompt, clip_skip[0], vae, vae_name, ckpt_name, lora_name, lora_model_wt, lora_clip_wt, 0, types, script_node_id, cache) # Generate Results @@ -1030,14 +1170,15 @@ class TSC_KSampler: seed_updated = seed + Y_index # Define Y parameters and generate labels - steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip, \ + steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, clip_skip, positive_prompt, negative_prompt, \ lora_name, lora_model_wt, lora_clip_wt, Y_label = \ - define_variable(Y_type, Y, seed_updated, steps, cfg, sampler_name, scheduler, denoise, vae_name, - ckpt_name, clip_skip, lora_name, lora_model_wt, lora_clip_wt, Y_label, len(Y_value)) + define_variable(Y_type, Y, seed_updated, steps, cfg, sampler_name, scheduler, denoise, vae_name, ckpt_name, + clip_skip, positive_prompt, negative_prompt, lora_name, lora_model_wt, lora_clip_wt, + Y_label, len(Y_value)) # Models & Conditionings model, positive, negative, vae = \ - define_model(model, clip, positive, negative, positive_prompt, negative_prompt, clip_skip, vae, + define_model(model, clip, positive, negative, positive_prompt, negative_prompt, clip_skip[0], vae, vae_name, ckpt_name, lora_name, lora_model_wt, lora_clip_wt, Y_index, types, script_node_id, cache) # Generate Results @@ -1053,26 +1194,39 @@ class TSC_KSampler: # Prioritrize Caching Checkpoints over LoRAs. if X_type == "LoRA": clear_cache_by_exception(script_node_id, ckpt_dict=[]) - if Y_type == "LoRA": # This implies X_type == "Checkpoint" + 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_name, vae_name, - clip_skip, steps, cfg, sampler_name, scheduler, denoise, latent_height, latent_width): - #print("\n" + "-" * 40) # Print an empty line followed by a separator line + def print_plot_variables(X_type, Y_type, X_value, Y_value, seed, ckpt_name, lora_name, lora_model_wt, lora_clip_wt, + 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 Settings:\033[0m") - print("- " * 20) # Print an empty line followed by a separator line + print("\033[32mXY Plot Results:\033[0m") - if X_type == "Checkpoint" or Y_type == "Checkpoint": - ckpt_name = ", ".join([str(x[0]) for x in X_value]) if X_type == "Checkpoint" else ckpt_name - clip_skip = ", ".join([str(x[1]) for x in X_value]) if X_type == "Checkpoint" else clip_skip + if X_type == "Checkpoint": + if Y_type == "Clip Skip": + ckpt_name = ", ".join([os.path.basename(str(x[0])) for x in X_value]) if X_type == "Checkpoint" else ckpt_name + else: + ckpt_name = ", ".join([f"{os.path.basename(str(x[0]))}({str(x[1]) if x[1] is not None else str(clip_skip[1])})" + for x in X_value]) if X_type == "Checkpoint" else ckpt_name + clip_skip = "_" - lora_name = ", ".join([str(x[0]) for x in X_value]) if X_type == "LoRA" else ", ".join( - [str(y[0]) for y in Y_value]) if Y_type == "LoRA" else lora_name + clip_skip = ", ".join(map(str, X_value)) if X_type == "Clip Skip" else ", ".join( + map(str, Y_value)) if Y_type == "Clip Skip" else clip_skip - vae_name = ", ".join(X_value) if X_type == "VAE" else vae_name - ckpt_name = ", ".join(Y_value) if Y_type == "VAE" else ckpt_name + if X_type != "LoRA" and Y_type != "LoRA": + if lora_name: + lora_name = f"{os.path.basename(lora_name)}({lora_model_wt},{lora_clip_wt})" + else: + lora_name = ", ".join([f"{os.path.basename(str(x[0]))}({str(x[1])},{str(x[2])})" for x in X_value])\ + if X_type == "LoRA" else ", ".join([f"{os.path.basename(str(y[0]))}({str(y[1])},{str(y[2])})" + for y in Y_value]) if Y_type == "LoRA" else lora_name + + vae_name = ", ".join( + map(lambda x: os.path.basename(str(x)), X_value)) if X_type == "VAE" else ", ".join( + map(lambda y: os.path.basename(str(y)), Y_value)) if Y_type == "VAE" else vae_name 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 [ @@ -1085,31 +1239,57 @@ class TSC_KSampler: 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" or Y_type == "Sampler": - sampler_name = ", ".join([str(x[0]) for x in X_value]) if X_type == "Sampler" else sampler_name - scheduler = ", ".join([str(x[1]) for x in X_value]) if X_type == "Sampler" else scheduler + if X_type == "Sampler": + sampler_name = ", ".join([f"{x[0]}({x[1] if x[1] is not None else scheduler[1]})" for x in X_value]) + scheduler = "_" + elif Y_type == "Sampler": + sampler_name = ", ".join([f"{y[0]}({y[1] if y[1] is not None else scheduler[1]})" for y in Y_value]) + scheduler = "_" scheduler = ", ".join([str(x[0]) for x in X_value]) if X_type == "Scheduler" else ", ".join( [str(y[0]) for y in Y_value]) if Y_type == "Scheduler" else scheduler + if isinstance(scheduler, tuple): + scheduler = 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"dim: {latent_height} x {latent_width}") - print(f"ckpt_name: {ckpt_name if ckpt_name is not None else '?'}") - print(f"lora_name: {lora_name}") - print(f"vae_name: {vae_name if vae_name is not None else '?'}") - print(f"clip_skip: {clip_skip if clip_skip is not None else '?'}") + 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[1] 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}") - print(f"sampler_name: {sampler_name}") - print(f"scheduler: {scheduler}") + if scheduler == "_": + print(f"sampler(schr): {sampler_name}") + else: + print(f"sampler: {sampler_name}") + print(f"scheduler: {scheduler}") print(f"denoise: {denoise}") - print_plot_variables(X_type, Y_type, X_value, Y_value, seed, ckpt_name, lora_name, vae_name, clip_skip, - steps, cfg, sampler_name, scheduler[0], denoise, latent_height, latent_width) + 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): @@ -1129,16 +1309,6 @@ class TSC_KSampler: # Disable vae decode on next Hold update_value_by_id("vae_decode", my_unique_id, False) - # 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 - - # 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) - def rearrange_list_A(arr, num_cols, num_rows): new_list = [] for i in range(num_rows): @@ -1155,12 +1325,26 @@ class TSC_KSampler: new_list.append(arr[index]) return new_list - # Rearrange lists for proper display - if flip_xy == False: - latent_list = rearrange_list_A(latent_list, num_cols, num_rows) - else: - latent_list = rearrange_list_B(latent_list, num_cols, num_rows) + # 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_name, lora_model_wt, lora_clip_wt, + 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) @@ -1269,7 +1453,7 @@ class TSC_KSampler: 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, font_size), color=(255, 255, 255, 0)) + 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 @@ -1310,19 +1494,22 @@ class TSC_KSampler: images = pil2tensor(background) - # Generate image results and store + # 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 - image_tensor_list = torch.stack([tensor.squeeze() for tensor in image_tensor_list]) + 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("\n" + "-" * 40) # Print an empty line followed by a separator line + print("-" * 40) # Print an empty line followed by a separator line # Output image results to ui and node outputs return {"ui": {"images": results}, "result": (model, positive, negative, {"samples": latent_list}, vae, image_tensor_list,)} @@ -1340,42 +1527,45 @@ class TSC_XYplot: "cache_models": (["True", "False"],),}, "optional": {"dependencies": ("DEPENDENCIES", ), "X": ("XY", ), - "Y": ("XY", ),}, - "hidden": {"my_unique_id": "UNIQUE_ID",},} + "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, dependencies=None, X=None, Y=None, my_unique_id=None): + def XYplot(self, grid_spacing, XY_flip, Y_label_orientation, cache_models, dependencies=None, X=None, Y=None): # Unpack X & Y Tuples if connected - if X is not None: + if X != None: X_type, X_value = X else: X_type = "Nothing" X_value = [""] - if Y is not None: + if Y != None: Y_type, Y_value = Y else: Y_type = "Nothing" Y_value = [""] - # Nothing is connected or Error - if X == Y == None or X_type == "Error" or Y_type == "Error": - return (None,) - - # If types are the same, error and return - if (X_type == Y_type) and (X_type != "Nothing"): - print(f"\033[31mXY Plot Error:\033[0m X and Y must be different.") - # Return None + # 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.") + else: + # Print XY Plot Inputs + print("-" * 40) + print("XY Plot Script Inputs:") + print(f"(X) {X_type}: {X_value}") + print(f"(Y) {Y_type}: {Y_value}") + print("-" * 40) return (None,) # Check that dependencies is connected for Checkpoint and LoRA plots - if X_type == "Checkpoint" or Y_type == "Checkpoint" or X_type == "LoRA" or Y_type == "LoRA": + 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 Checkpoint/LoRA plots.") + print(f"\033[31mXY Plot Error:\033[0m The dependencies input must be connected for certain plot types.") # Return None return (None,) @@ -1392,7 +1582,6 @@ class TSC_XYplot: X_type, Y_type = Y_type, X_type X_value, Y_value = Y_value, X_value - return ((X_type, X_value, Y_type, Y_value, grid_spacing, Y_label_orientation, cache_models, dependencies),) @@ -1602,6 +1791,80 @@ class TSC_XYplot_VAE: return (None,) return ((xy_type, xy_value),) +# TSC XY Plot: Prompt S/R +class TSC_XYplot_PromptSR_Positive: + + @classmethod + 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),) + +class TSC_XYplot_PromptSR_Negative: + + @classmethod + 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: @@ -1639,6 +1902,33 @@ class TSC_XYplot_Checkpoint: return (None,) return ((xy_type, xy_value),) +# TSC XY Plot: Clip Skip +class TSC_XYplot_ClipSkip: + + @classmethod + 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: @@ -1711,6 +2001,399 @@ class TSC_XYplot_LoRA_Adv: 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")) + + @classmethod + 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: + + @classmethod + 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 != "Positive Prompt S/R" and X_type != "Negative Prompt S/R": + 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 != "Positive Prompt S/R" and Y_type != "Negative Prompt S/R": + 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,) + + # Clean Schedulers from Sampler data (if other type is Scheduler) + if X_type == "Sampler" and Y_type == "Scheduler": + # Clear X_value Scheduler's + X_value = [[x[0], ""] for x in X_value] + elif Y_type == "Sampler" and X_type == "Scheduler": + # Clear Y_value Scheduler's + Y_value = [[y[0], ""] for y in Y_value] + + # Clean X/Y_values + 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: @@ -1732,11 +2415,13 @@ class TSC_XYplot_JoinInputs: if xy_type_1 != xy_type_2: print(f"\033[31mJoin XY Inputs Error:\033[0m Input types must match") - xy_type = "Error" - xy_value = "" + 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 @@ -1946,10 +2631,15 @@ NODE_CLASS_MAPPINGS = { "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 (Advanced)": TSC_XYplot_LoRA_Adv, - "Join XY Inputs": TSC_XYplot_JoinInputs, + "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,