diff --git a/README.md b/README.md index 42ec53e..bb46036 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Efficiency Nodes for ComfyUI ======= ### A collection of ComfyUI custom nodes to help streamline workflows and reduce total node count. -## [Direct Download Link](https://github.com/LucianoCirino/efficiency-nodes-comfyui/releases/download/v1.92/efficiency-nodes-comfyui-.v192.7z) +## [Direct Download Link](https://github.com/LucianoCirino/efficiency-nodes-comfyui/releases/download/v1.92/efficiency-nodes-comfyui-v192.7z)
Efficient Loader diff --git a/__init__.py b/__init__.py index d8edad8..806f466 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,3 @@ -from .efficiency_nodes import NODE_CLASS_MAPPINGS - -__all__ = ['NODE_CLASS_MAPPINGS'] +from .efficiency_nodes import NODE_CLASS_MAPPINGS +WEB_DIRECTORY = "js" +__all__ = ['NODE_CLASS_MAPPINGS'] diff --git a/efficiency_nodes.py b/efficiency_nodes.py index df8b23e..9754eb7 100644 --- a/efficiency_nodes.py +++ b/efficiency_nodes.py @@ -28,7 +28,8 @@ font_path = os.path.join(my_dir, 'arial.ttf') # Append comfy_dir to sys.path & import files sys.path.append(comfy_dir) -from nodes import LatentUpscaleBy, KSampler, KSamplerAdvanced, VAEDecode, VAEDecodeTiled, CLIPSetLastLayer, CLIPTextEncode, ControlNetApplyAdvanced +from nodes import LatentUpscaleBy, KSampler, KSamplerAdvanced, VAEDecode, VAEDecodeTiled, VAEEncode, VAEEncodeTiled, \ + ImageScaleBy, CLIPSetLastLayer, CLIPTextEncode, ControlNetLoader, ControlNetApply, ControlNetApplyAdvanced from comfy_extras.nodes_clip_sdxl import CLIPTextEncodeSDXL, CLIPTextEncodeSDXLRefiner import comfy.samplers import comfy.sd @@ -81,11 +82,11 @@ def encode_prompts(positive_prompt, negative_prompt, clip, clip_skip, refiner_cl empty_latent_height, negative_prompt)[0] # Return results based on return_type if return_type == "base": - return positive_encoded, negative_encoded + return positive_encoded, negative_encoded, clip elif return_type == "refiner": - return refiner_positive_encoded, refiner_negative_encoded + return refiner_positive_encoded, refiner_negative_encoded, refiner_clip elif return_type == "both": - return positive_encoded, negative_encoded, refiner_positive_encoded, refiner_negative_encoded + return positive_encoded, negative_encoded, clip, refiner_positive_encoded, refiner_negative_encoded, refiner_clip ######################################################################################################################## # TSC Efficient Loader @@ -162,7 +163,7 @@ class TSC_EfficientLoader: clip_skip = clip_skip[0] if loader_type == "sdxl" else clip_skip # Encode prompt based on loader_type - positive_encoded, negative_encoded, refiner_positive_encoded, refiner_negative_encoded = \ + positive_encoded, negative_encoded, clip, refiner_positive_encoded, refiner_negative_encoded, refiner_clip = \ encode_prompts(positive, negative, clip, clip_skip, refiner_clip, refiner_clip_skip, ascore, loader_type == "sdxl", empty_latent_width, empty_latent_height) @@ -373,10 +374,11 @@ class TSC_Apply_ControlNet_Stack: for control_net_tuple in cnet_stack: control_net, image, strength, start_percent, end_percent = control_net_tuple - controlnet_conditioning = ControlNetApplyAdvanced().apply_controlnet(positive, negative, - control_net, image, strength, - start_percent, end_percent) - return controlnet_conditioning + controlnet_conditioning = ControlNetApplyAdvanced().apply_controlnet(positive, negative, control_net, image, + strength, start_percent, end_percent) + positive, negative = controlnet_conditioning[0], controlnet_conditioning[1] + + return (positive, negative, ) ######################################################################################################################## # TSC KSampler (Efficient) @@ -391,7 +393,7 @@ class TSC_KSampler: @classmethod def INPUT_TYPES(cls): return {"required": - {"sampler_state": (["Sample", "Hold", "Script"], ), + {"sampler_state": (["Sample", "Script", "Hold"], ), "model": ("MODEL",), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), @@ -532,36 +534,23 @@ class TSC_KSampler: def vae_decode_latent(vae, samples, vae_decode): return VAEDecodeTiled().decode(vae,samples,512)[0] if "tiled" in vae_decode else VAEDecode().decode(vae,samples)[0] + def vae_encode_image(vae, pixels, vae_decode): + return VAEEncodeTiled().encode(vae,pixels,512)[0] if "tiled" in vae_decode else VAEEncode().encode(vae,pixels)[0] + # --------------------------------------------------------------------------------------------------------------- def sample_latent_image(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, sampler_type, add_noise, start_at_step, end_at_step, return_with_leftover_noise, - refiner_model=None, refiner_positive=None, refiner_negative=None): - - if keys_exist_in_script("tile"): - tile_width, tile_height, tiling_strategy, blenderneko_tiled_nodes = script["tile"] - TSampler = blenderneko_tiled_nodes.TiledKSampler - TSamplerAdvanced = blenderneko_tiled_nodes.TiledKSamplerAdvanced + refiner_model, refiner_positive, refiner_negative, vae, vae_decode, sampler_state): # Sample the latent_image(s) using the Comfy KSampler nodes if sampler_type == "regular": - if keys_exist_in_script("tile"): - samples = TSampler().sample(model, seed, tile_width, tile_height, tiling_strategy, steps, cfg, - sampler_name, scheduler, positive, negative, - latent_image, denoise=denoise)[0] - else: - samples = KSampler().sample(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, - latent_image, denoise=denoise)[0] + samples = KSampler().sample(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, + latent_image, denoise=denoise)[0] elif sampler_type == "advanced": - if keys_exist_in_script("tile"): - samples = TSamplerAdvanced().sample(model, add_noise, seed, tile_width, tile_height, tiling_strategy, - steps, cfg, sampler_name, scheduler, - positive, negative, latent_image, start_at_step, end_at_step, - return_with_leftover_noise, "disabled", denoise=1.0)[0] - else: - samples = KSamplerAdvanced().sample(model, add_noise, seed, steps, cfg, sampler_name, scheduler, - positive, negative, latent_image, start_at_step, end_at_step, - return_with_leftover_noise, denoise=1.0)[0] + samples = KSamplerAdvanced().sample(model, add_noise, seed, steps, cfg, sampler_name, scheduler, + positive, negative, latent_image, start_at_step, end_at_step, + return_with_leftover_noise, denoise=1.0)[0] elif sampler_type == "sdxl": # Disable refiner if refine_at_step is -1 @@ -582,24 +571,35 @@ class TSC_KSampler: samples, end_at_step, steps, return_with_leftover_noise, denoise=1.0)[0] - # Check if "hiresfix" exists in the script after main sampling has taken place - if keys_exist_in_script("hiresfix"): + if sampler_state == "Script": - # Unpack the tuple from the script's "hiresfix" key - latent_upscale_method, upscale_by, hires_steps, hires_denoise, iterations, upscale_function = script["hiresfix"] - - # Iterate for the given number of iterations - for _ in range(iterations): - upscaled_latent_image = upscale_function().upscale(samples, latent_upscale_method, upscale_by)[0] - # Use the regular KSampler for each iteration - if False: #if keys_exist_in_script("tile"): # Disabled for HiResFix - samples = TSampler().sample(model, seed, tile_width, tile_height, tiling_strategy, steps, cfg, - sampler_name, scheduler, - positive, negative, upscaled_latent_image, denoise=hires_denoise)[0] - else: + # Check if "hiresfix" exists in the script after main sampling has taken place + if keys_exist_in_script("hiresfix"): + # Unpack the tuple from the script's "hiresfix" key + latent_upscale_method, upscale_by, hires_steps, hires_denoise, iterations, upscale_function = script["hiresfix"] + # Iterate for the given number of iterations + for _ in range(iterations): + upscaled_latent_image = upscale_function().upscale(samples, latent_upscale_method, upscale_by)[0] samples = KSampler().sample(model, seed, hires_steps, cfg, sampler_name, scheduler, - positive, negative, upscaled_latent_image, denoise=hires_denoise)[0] + positive, negative, upscaled_latent_image, denoise=hires_denoise)[0] + # Check if "tile" exists in the script after main sampling has taken place + if keys_exist_in_script("tile"): + # Unpack the tuple from the script's "tile" key + upscale_by, tile_controlnet, tile_size, tiling_strategy, tiling_steps, tiled_denoise,\ + blenderneko_tiled_nodes = script["tile"] + # VAE Decode samples + image = vae_decode_latent(vae, samples, vae_decode) + # Upscale image + upscaled_image = ImageScaleBy().upscale(image, "nearest-exact", upscale_by)[0] + upscaled_latent = vae_encode_image(vae, upscaled_image, vae_decode) + # Apply Control Net using upscaled_image and loaded control_net + positive = ControlNetApply().apply_controlnet(positive, tile_controlnet, upscaled_image, 1)[0] + # Sample latent + TSampler = blenderneko_tiled_nodes.TiledKSampler + samples = TSampler().sample(model, seed-1, tile_size, tile_size, tiling_strategy, tiling_steps, cfg, + sampler_name, scheduler, positive, negative, upscaled_latent, + denoise=tiled_denoise)[0] return samples # --------------------------------------------------------------------------------------------------------------- @@ -643,8 +643,9 @@ class TSC_KSampler: send_command_to_frontend(startListening=True, maxCount=steps-1, sendBlob=False) samples = sample_latent_image(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, - latent_image, denoise, sampler_type, add_noise, start_at_step, end_at_step, - return_with_leftover_noise, refiner_model, refiner_positive, refiner_negative) + latent_image, denoise, sampler_type, add_noise, start_at_step, end_at_step, + return_with_leftover_noise, refiner_model, refiner_positive, refiner_negative, + vae, vae_decode, sampler_state) # Cache samples in the 'last_helds' dictionary "latent" update_value_by_id("latent", my_unique_id, samples) @@ -854,7 +855,7 @@ class TSC_KSampler: else (os.path.basename(v[0]), v[1]) if v[2] is None else (os.path.basename(v[0]),) + v[1:] for v in value] - elif type_ == "LoRA" and isinstance(value, list): + elif (type_ == "LoRA" or type_ == "LoRA Stacks") and isinstance(value, list): # Return only the first Tuple of each inner array return [[(os.path.basename(v[0][0]),) + v[0][1:], "..."] if len(v) > 1 else [(os.path.basename(v[0][0]),) + v[0][1:]] for v in value] @@ -955,6 +956,7 @@ class TSC_KSampler: "Checkpoint", "Refiner", "LoRA", + "LoRA Stacks", "VAE", ] conditioners = { @@ -999,6 +1001,9 @@ class TSC_KSampler: # Create a list of tuples with types and values type_value_pairs = [(X_type, X_value.copy()), (Y_type, Y_value.copy())] + # Replace "LoRA Stacks" with "LoRA" + type_value_pairs = [('LoRA' if t == 'LoRA Stacks' else t, v) for t, v in type_value_pairs] + # Iterate over type-value pairs for t, v in type_value_pairs: if t in dict_map: @@ -1041,7 +1046,7 @@ class TSC_KSampler: elif X_type == "Refiner": ckpt_dict = [] lora_dict = [] - elif X_type == "LoRA": + elif X_type in ("LoRA", "LoRA Stacks"): ckpt_dict = [] refn_dict = [] @@ -1204,7 +1209,7 @@ class TSC_KSampler: text = f"RefClipSkip ({refiner_clip_skip[0]})" elif "LoRA" in var_type: - if not lora_stack: + if not lora_stack or var_type == "LoRA Stacks": lora_stack = var.copy() else: # Updating the first tuple of lora_stack @@ -1214,7 +1219,7 @@ class TSC_KSampler: lora_name, lora_model_wt, lora_clip_wt = lora_stack[0] lora_filename = os.path.splitext(os.path.basename(lora_name))[0] - if var_type == "LoRA": + if var_type == "LoRA" or var_type == "LoRA Stacks": if len(lora_stack) == 1: lora_model_wt = format(float(lora_model_wt), ".2f").rstrip('0').rstrip('.') lora_clip_wt = format(float(lora_clip_wt), ".2f").rstrip('0').rstrip('.') @@ -1337,7 +1342,7 @@ class TSC_KSampler: # Note: Index is held at 0 when Y_type == "Nothing" # 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 (X_type == "Checkpoint" and index == 0 and Y_type not in ("LoRA", "LoRA Stacks")): if lora_stack is None: model, clip, _ = load_checkpoint(ckpt_name, xyplot_id, cache=cache[1]) else: # Load Efficient Loader LoRA @@ -1346,11 +1351,11 @@ class TSC_KSampler: encode = True # Load LoRA if required - elif (X_type == "LoRA" and index == 0): + elif (X_type in ("LoRA", "LoRA Stacks") and index == 0): # Don't cache Checkpoints model, clip = load_lora(lora_stack, ckpt_name, xyplot_id, cache=cache[2]) encode = True - elif Y_type == "LoRA": # X_type must be Checkpoint, so cache those as defined + elif Y_type in ("LoRA", "LoRA Stacks"): # X_type must be Checkpoint, so cache those as defined model, clip = load_lora(lora_stack, ckpt_name, xyplot_id, cache=None, ckpt_cache=cache[1]) encode = True @@ -1378,7 +1383,7 @@ class TSC_KSampler: # Encode base prompt if encode == True: - positive, negative = \ + positive, negative, clip = \ encode_prompts(positive_prompt, negative_prompt, clip, clip_skip, refiner_clip, refiner_clip_skip, ascore, sampler_type == "sdxl", empty_latent_width, empty_latent_height, return_type="base") @@ -1388,7 +1393,7 @@ class TSC_KSampler: positive, negative = controlnet_conditioning[0], controlnet_conditioning[1] if encode_refiner == True: - refiner_positive, refiner_negative = \ + refiner_positive, refiner_negative, refiner_clip = \ encode_prompts(positive_prompt, negative_prompt, clip, clip_skip, refiner_clip, refiner_clip_skip, ascore, sampler_type == "sdxl", empty_latent_width, empty_latent_height, return_type="refiner") @@ -1424,7 +1429,8 @@ class TSC_KSampler: samples = sample_latent_image(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise, sampler_type, add_noise, start_at_step, end_at_step, - return_with_leftover_noise, refiner_model, refiner_positive, refiner_negative) + return_with_leftover_noise, refiner_model, refiner_positive, refiner_negative, + vae, vae_decode, sampler_state) # Add the latent tensor to the tensors list latent_list.append(samples) @@ -1494,7 +1500,6 @@ class TSC_KSampler: ckpt_name, clip_skip, refiner_name, refiner_clip_skip, positive_prompt, negative_prompt, ascore, lora_stack, cnet_stack, X_label, len(X_value)) - if X_type != "Nothing" and Y_type == "Nothing": if X_type == "XY_Capsule": model, clip, refiner_model, refiner_clip = \ @@ -1520,10 +1525,11 @@ class TSC_KSampler: elif X_type != "Nothing" and Y_type != "Nothing": for Y_index, Y in enumerate(Y_value): - - if Y_type == "XY_Capsule" and X_type == "XY_Capsule": + if Y_type == "XY_Capsule" or X_type == "XY_Capsule": model, clip, refiner_model, refiner_clip = \ clone_or_none(original_model, original_clip, original_refiner_model, original_refiner_clip) + + if Y_type == "XY_Capsule" and X_type == "XY_Capsule": Y.set_x_capsule(X) # Define Y parameters and generate labels @@ -1569,7 +1575,7 @@ class TSC_KSampler: clear_cache_by_exception(xyplot_id, lora_dict=[], refn_dict=[]) elif X_type == "Refiner": clear_cache_by_exception(xyplot_id, ckpt_dict=[], lora_dict=[]) - elif X_type == "LoRA": + elif X_type in ("LoRA", "LoRA Stacks"): clear_cache_by_exception(xyplot_id, ckpt_dict=[], refn_dict=[]) # __________________________________________________________________________________________________________ @@ -1669,7 +1675,7 @@ class TSC_KSampler: lora_name = lora_wt = lora_model_str = lora_clip_str = None # Check for all possible LoRA types - lora_types = ["LoRA", "LoRA Batch", "LoRA Wt", "LoRA MStr", "LoRA CStr"] + lora_types = ["LoRA", "LoRA Stacks", "LoRA Batch", "LoRA Wt", "LoRA MStr", "LoRA CStr"] if X_type not in lora_types and Y_type not in lora_types: if lora_stack: @@ -1682,7 +1688,7 @@ class TSC_KSampler: else: if X_type in lora_types: value = get_lora_sublist_name(X_type, X_value) - if X_type == "LoRA": + if X_type in ("LoRA", "LoRA Stacks"): lora_name = value lora_model_str = None lora_clip_str = None @@ -1704,7 +1710,7 @@ class TSC_KSampler: if Y_type in lora_types: value = get_lora_sublist_name(Y_type, Y_value) - if Y_type == "LoRA": + if Y_type in ("LoRA", "LoRA Stacks"): lora_name = value lora_model_str = None lora_clip_str = None @@ -1727,13 +1733,13 @@ class TSC_KSampler: return lora_name, lora_wt, lora_model_str, lora_clip_str def get_lora_sublist_name(lora_type, lora_value): - if lora_type == "LoRA" or lora_type == "LoRA Batch": + if lora_type in ("LoRA", "LoRA Batch", "LoRA Stacks"): formatted_sublists = [] for sublist in lora_value: formatted_entries = [] for x in sublist: base_name = os.path.splitext(os.path.basename(str(x[0])))[0] - formatted_str = f"{base_name}({round(x[1], 3)},{round(x[2], 3)})" if lora_type == "LoRA" else f"{base_name}" + formatted_str = f"{base_name}({round(x[1], 3)},{round(x[2], 3)})" if lora_type in ("LoRA", "LoRA Stacks") else f"{base_name}" formatted_entries.append(formatted_str) formatted_sublists.append(f"{', '.join(formatted_entries)}") return "\n ".join(formatted_sublists) @@ -2376,7 +2382,7 @@ class TSC_XYplot: # Check that dependencies are connected for specific plot types encode_types = { "Checkpoint", "Refiner", - "LoRA", "LoRA Batch", "LoRA Wt", "LoRA MStr", "LoRA CStr", + "LoRA", "LoRA Stacks", "LoRA Batch", "LoRA Wt", "LoRA MStr", "LoRA CStr", "Positive Prompt S/R", "Negative Prompt S/R", "AScore+", "AScore-", "Clip Skip", "Clip Skip (Refiner)", @@ -2392,8 +2398,13 @@ class TSC_XYplot: # Check if both X_type and Y_type are special lora_types lora_types = {"LoRA Batch", "LoRA Wt", "LoRA MStr", "LoRA CStr"} if (X_type in lora_types and Y_type not in lora_types) or (Y_type in lora_types and X_type not in lora_types): - print( - f"{error('XY Plot Error:')} Both X and Y must be connected to use the 'LoRA Plot' node.") + print(f"{error('XY Plot Error:')} Both X and Y must be connected to use the 'LoRA Plot' node.") + return (None,) + + # Do not allow LoRA and LoRA Stacks + lora_types = {"LoRA", "LoRA Stacks"} + if (X_type in lora_types and Y_type in lora_types): + print(f"{error('XY Plot Error:')} X and Y input types must be different.") return (None,) # Clean Schedulers from Sampler data (if other type is Scheduler) @@ -2514,7 +2525,7 @@ class TSC_XYplot_Steps: xy_type = "Steps" xy_first = first_step xy_last = last_step - elif target_parameter == "start at step": + elif target_parameter == "start_at_step": xy_type = "StartStep" xy_first = first_start_step xy_last = last_start_step @@ -3140,7 +3151,7 @@ class TSC_XYplot_LoRA_Stacks: CATEGORY = "Efficiency Nodes/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_type = "LoRA Stacks" 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,) @@ -3312,7 +3323,7 @@ class TSC_XYplot_Control_Net_End: # ======================================================================================================================= # TSC XY Plot: Control Net -class TSC_XYplot_Control_Net: +class TSC_XYplot_Control_Net(TSC_XYplot_Control_Net_Strength, TSC_XYplot_Control_Net_Start, TSC_XYplot_Control_Net_End): parameters = ["strength", "start_percent", "end_percent"] @classmethod def INPUT_TYPES(cls): @@ -3329,7 +3340,7 @@ class TSC_XYplot_Control_Net: "first_end_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}), "last_end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), "strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}), - "start_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}), "end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), }, "optional": {"cnet_stack": ("CONTROL_NET_STACK",)}, @@ -3341,10 +3352,17 @@ class TSC_XYplot_Control_Net: CATEGORY = "Efficiency Nodes/XY Inputs" def xy_value(self, control_net, image, target_parameter, batch_count, first_strength, last_strength, first_start_percent, - last_start_percent, first_end_percent, last_end_percent, strength, start_percent, cnet_stack=None): + last_start_percent, first_end_percent, last_end_percent, strength, start_percent, end_percent, cnet_stack=None): - - return ((xy_type, xy_value),) + if target_parameter == "strength": + return TSC_XYplot_Control_Net_Strength.xy_value(self, control_net, image, batch_count, first_strength, + last_strength, start_percent, end_percent, cnet_stack=cnet_stack) + elif target_parameter == "start_percent": + return TSC_XYplot_Control_Net_Start.xy_value(self, control_net, image, batch_count, first_start_percent, + last_start_percent, strength, end_percent, cnet_stack=cnet_stack) + elif target_parameter == "end_percent": + return TSC_XYplot_Control_Net_End.xy_value(self, control_net, image, batch_count, first_end_percent, + last_end_percent, strength, start_percent, cnet_stack=cnet_stack) #======================================================================================================================= # TSC XY Plot: Control Net Plot @@ -4010,8 +4028,8 @@ NODE_CLASS_MAPPINGS = { "XY Input: LoRA Stacks": TSC_XYplot_LoRA_Stacks, "XY Input: Control Net": TSC_XYplot_Control_Net, "XY Input: Control Net Plot": TSC_XYplot_Control_Net_Plot, - "XY Input: Manual XY Entry": TSC_XYplot_Manual_XY_Entry, # DISABLED, NEEDS UPDATE - "Manual XY Entry Info": TSC_XYplot_Manual_XY_Entry_Info, # DISABLED, NEEDS UPDATE + "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 } @@ -4110,7 +4128,7 @@ class TSC_HighRes_Fix: NODE_CLASS_MAPPINGS.update({"HighRes-Fix Script": TSC_HighRes_Fix}) ######################################################################################################################## -'''# FUTURE +''' # Tiled Sampling KSamplers (https://github.com/BlenderNeko/ComfyUI_TiledKSampler) blenderneko_tiled_ksampler_path = os.path.join(custom_nodes_dir, "ComfyUI_TiledKSampler") if os.path.exists(blenderneko_tiled_ksampler_path): @@ -4120,14 +4138,20 @@ if os.path.exists(blenderneko_tiled_ksampler_path): blenderneko_tiled_nodes = import_module("ComfyUI_TiledKSampler.nodes") print(f"\r{message('Efficiency Nodes:')} {printout}{success('Success!')}") - # TSC Tiled Sampling - class TSC_Tiled_Sampling: - + # TSC Tiled Upscaler + class TSC_Tiled_Upscaler: @classmethod def INPUT_TYPES(cls): - return {"required": {"tile_width": ("INT", {"default": 512, "min": 256, "max": MAX_RESOLUTION, "step": 64}), - "tile_height": ("INT", {"default": 512, "min": 256, "max": MAX_RESOLUTION, "step": 64}), + # Split the list based on the keyword "tile" + cnet_tile_filenames = [name for name in folder_paths.get_filename_list("controlnet") if "tile" in name] + cnet_other_filenames = [name for name in folder_paths.get_filename_list("controlnet") if "tile" not in name] + + return {"required": {"tile_controlnet": (cnet_tile_filenames + cnet_other_filenames,), + "upscale_by": ("FLOAT", {"default": 1.25, "min": 0.01, "max": 8.0, "step": 0.25}), + "tile_size": ("INT", {"default": 512, "min": 256, "max": MAX_RESOLUTION, "step": 64}), "tiling_strategy": (["random", "random strict", "padded", 'simple', 'none'],), + "tiling_steps": ("INT", {"default": 30, "min": 1, "max": 10000}), + "denoise": ("FLOAT", {"default": .56, "min": 0.0, "max": 1.0, "step": 0.01}), }, "optional": {"script": ("SCRIPT",)}} @@ -4135,13 +4159,14 @@ if os.path.exists(blenderneko_tiled_ksampler_path): FUNCTION = "tiled_sampling" CATEGORY = "Efficiency Nodes/Scripts" - def tiled_sampling(self, tile_width, tile_height, tiling_strategy, script=None): + def tiled_sampling(self, upscale_by, tile_controlnet, tile_size, tiling_strategy, tiling_steps, denoise, script=None): if tiling_strategy != 'none': script = script or {} - script["tile"] = (tile_width, tile_height, tiling_strategy, blenderneko_tiled_nodes) + script["tile"] = (upscale_by, ControlNetLoader().load_controlnet(tile_controlnet)[0], + tile_size, tiling_strategy, tiling_steps, denoise, blenderneko_tiled_nodes) return (script,) - NODE_CLASS_MAPPINGS.update({"Tiled Sampling Script": TSC_Tiled_Sampling}) + NODE_CLASS_MAPPINGS.update({"Tiled Upscaler Script": TSC_Tiled_Upscaler}) except ImportError: print(f"\r{message('Efficiency Nodes:')} {printout}{error('Failed!')}") diff --git a/js/appearance.js b/js/appearance.js index cb15850..5ea8599 100644 --- a/js/appearance.js +++ b/js/appearance.js @@ -63,6 +63,7 @@ let colorKeys = Object.keys(COLOR_THEMES).filter(key => key !== "none"); shuffleArray(colorKeys); // Shuffle the color themes initially function setNodeColors(node, theme) { + if (!theme) {return;} node.shape = "box"; if(theme.nodeColor && theme.nodeBgColor) { node.color = theme.nodeColor; @@ -79,9 +80,10 @@ const ext = { let colorKey = NODE_COLORS[title]; if (colorKey === "random") { - if (colorKeys.length === 0) { - colorKeys = Object.values(COLOR_THEMES).filter(theme => theme.nodeColor && theme.nodeBgColor); - shuffleArray(colorKeys); // Reshuffle when out of colors + // Check for a valid color key before popping + if (colorKeys.length === 0 || !COLOR_THEMES[colorKeys[colorKeys.length - 1]]) { + colorKeys = Object.keys(COLOR_THEMES).filter(key => key !== "none"); + shuffleArray(colorKeys); } colorKey = colorKeys.pop(); } diff --git a/tsc_utils.py b/tsc_utils.py index 7d62f10..87d8777 100644 --- a/tsc_utils.py +++ b/tsc_utils.py @@ -472,10 +472,20 @@ def global_preview_method(): #----------------------------------------------------------------------------------------------------------------------- # Auto install Efficiency Nodes Python package dependencies import subprocess -# Note: Auto installer install packages inside the requirements.txt. -# It first trys ComfyUI's python_embedded folder if python.exe exists inside ...\ComfyUI_windows_portable\python_embeded. -# If no python.exe is found, it attempts a general global pip install of packages. -# On an error, an user is directed to attempt manually installing the packages themselves. +# Note: This auto-installer attempts to import packages listed in the requirements.txt. +# If the import fails, indicating the package isn't installed, the installer proceeds to install the package. +# It first checks if python.exe exists inside the ...\ComfyUI_windows_portable\python_embeded directory. +# If python.exe is found in that location, it will use this embedded Python version for the installation. +# Otherwise, it uses the Python interpreter that's currently executing the script (via sys.executable) +# to attempt a general pip install of the packages. If any errors occur during installation, an error message is +# printed with the reason for the failure, and the user is directed to manually install the required packages. + +def is_package_installed(pkg_name): + try: + __import__(pkg_name) + return True + except ImportError: + return False def install_packages(my_dir): # Compute path to the target site-packages @@ -483,71 +493,48 @@ def install_packages(my_dir): embedded_python_exe = os.path.abspath(os.path.join(my_dir, '..', '..', '..', 'python_embeded', 'python.exe')) # If embedded_python_exe exists, target the installations. Otherwise, go untargeted. - use_embedded = os.path.exists(embedded_python_exe) + use_embedded = os.path.exists(embedded_python_exe) and embedded_python_exe == sys.executable # Load packages from requirements.txt with open(os.path.join(my_dir, 'requirements.txt'), 'r') as f: required_packages = [line.strip() for line in f if line.strip()] - installed_packages = packages(embedded_python_exe if use_embedded else None, versions=False) for pkg in required_packages: - if pkg not in installed_packages: - print(f"\033[32mEfficiency Nodes:\033[0m Installing required package '{pkg}'...", end='', flush=True) + if not is_package_installed(pkg): + printout = f"Installing required package '{pkg}'..." + print(f"{message('Efficiency Nodes:')} {printout}", end='', flush=True) + try: if use_embedded: # Targeted installation - subprocess.check_call(['pip', 'install', pkg, '--target=' + target_dir, '--no-warn-script-location', - '--disable-pip-version-check'], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + subprocess.check_call([embedded_python_exe, '-m', 'pip', 'install', pkg, '--target=' + target_dir, + '--no-warn-script-location', '--disable-pip-version-check'], + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=7) else: # Untargeted installation - subprocess.check_call(['pip', 'install', pkg], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) - print(f"\r\033[32mEfficiency Nodes:\033[0m Installing required package '{pkg}'... Installed!", flush=True) - - except subprocess.CalledProcessError as e: # Failed installation - base_message = f"\r\033[31mEfficiency Nodes Error:\033[0m Failed to install python package '{pkg}'. " - if e.stderr: - error_message = e.stderr.decode() - print(base_message + f"Error message: {error_message}") - else: - print(base_message + "\nPlease check your permissions, network connectivity, or try a manual installation.") - -def packages(python_exe=None, versions=False): - # Get packages of the active or embedded Python environment - if python_exe: - return [(r.decode().split('==')[0] if not versions else r.decode()) for r in - subprocess.check_output([python_exe, '-m', 'pip', 'freeze']).split()] - else: - return [(r.split('==')[0] if not versions else r) for r in subprocess.getoutput('pip freeze').splitlines()] + subprocess.check_call([sys.executable, "-m", "pip", 'install', pkg], + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=7) + print(f"\r{message('Efficiency Nodes:')} {printout}{success(' Installed!')}", flush=True) + except Exception as e: + print(f"\r{message('Efficiency Nodes:')} {printout}{error(' Failed!')}", flush=True) + print(f"{warning(str(e))}") + +def print_general_error_message(): + print(f"{message('Efficiency Nodes:')} An unexpected error occurred during the package installation process. {error('Failed!')}") + print(warning("Please try manually installing the required packages from the requirements.txt file.")) # Install missing packages install_packages(my_dir) #----------------------------------------------------------------------------------------------------------------------- -# Auto install efficiency nodes web extensions '\js\' to 'ComfyUI\web\extensions' +# Delete efficiency nodes web extensions from 'ComfyUI\web\extensions'. +# Pull https://github.com/comfyanonymous/ComfyUI/pull/1273 now allows defining web extensions through a dir path in init import shutil -# Source and destination directories -source_dir = os.path.join(my_dir, 'js') +# Destination directory destination_dir = os.path.join(comfy_dir, 'web', 'extensions', 'efficiency-nodes-comfyui') -# Create the destination directory if it doesn't exist -os.makedirs(destination_dir, exist_ok=True) - -# Get a list of all .js files in the source directory -source_files = [f for f in os.listdir(source_dir) if f.endswith('.js')] - -# Clear files in the destination directory that aren't in the source directory -for file_name in os.listdir(destination_dir): - if file_name not in source_files and file_name.endswith('.js'): - file_path = os.path.join(destination_dir, file_name) - os.unlink(file_path) - -# Iterate over all files in the source directory for copying -for file_name in source_files: - # Full paths for source and destination - source_path = os.path.join(source_dir, file_name) - destination_path = os.path.join(destination_dir, file_name) - - # Directly copy the file (this will overwrite if the file already exists) - shutil.copy2(source_path, destination_path) +# Check if the directory exists and delete it +if os.path.exists(destination_dir): + shutil.rmtree(destination_dir) #----------------------------------------------------------------------------------------------------------------------- # Establish a websocket connection to communicate with "efficiency-nodes.js" under: