""" @author: rgthree @title: Comfy Nodes @nickname: rgthree @description: A bunch of nodes I created that I also find useful. """ from glob import glob import json import os import shutil import re import random import execution from .py.log import log from .py.config import get_config_value from .py.rgthree_server import * from .py.context import RgthreeContext from .py.context_switch import RgthreeContextSwitch from .py.context_switch_big import RgthreeContextSwitchBig from .py.display_any import RgthreeDisplayAny, RgthreeDisplayInt from .py.lora_stack import RgthreeLoraLoaderStack from .py.seed import RgthreeSeed from .py.sdxl_empty_latent_image import RgthreeSDXLEmptyLatentImage from .py.power_prompt import RgthreePowerPrompt from .py.power_prompt_simple import RgthreePowerPromptSimple from .py.image_inset_crop import RgthreeImageInsetCrop from .py.context_big import RgthreeBigContext from .py.dynamic_context import RgthreeDynamicContext from .py.dynamic_context_switch import RgthreeDynamicContextSwitch from .py.ksampler_config import RgthreeKSamplerConfig from .py.sdxl_power_prompt_postive import RgthreeSDXLPowerPromptPositive from .py.sdxl_power_prompt_simple import RgthreeSDXLPowerPromptSimple from .py.any_switch import RgthreeAnySwitch from .py.context_merge import RgthreeContextMerge from .py.context_merge_big import RgthreeContextMergeBig from .py.image_comparer import RgthreeImageComparer from .py.power_lora_loader import RgthreePowerLoraLoader NODE_CLASS_MAPPINGS = { RgthreeBigContext.NAME: RgthreeBigContext, RgthreeContext.NAME: RgthreeContext, RgthreeContextSwitch.NAME: RgthreeContextSwitch, RgthreeContextSwitchBig.NAME: RgthreeContextSwitchBig, RgthreeContextMerge.NAME: RgthreeContextMerge, RgthreeContextMergeBig.NAME: RgthreeContextMergeBig, RgthreeDisplayInt.NAME: RgthreeDisplayInt, RgthreeDisplayAny.NAME: RgthreeDisplayAny, RgthreeLoraLoaderStack.NAME: RgthreeLoraLoaderStack, RgthreeSeed.NAME: RgthreeSeed, RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop, RgthreePowerPrompt.NAME: RgthreePowerPrompt, RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple, RgthreeKSamplerConfig.NAME: RgthreeKSamplerConfig, RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage, RgthreeSDXLPowerPromptPositive.NAME: RgthreeSDXLPowerPromptPositive, RgthreeSDXLPowerPromptSimple.NAME: RgthreeSDXLPowerPromptSimple, RgthreeAnySwitch.NAME: RgthreeAnySwitch, RgthreeImageComparer.NAME: RgthreeImageComparer, RgthreePowerLoraLoader.NAME: RgthreePowerLoraLoader, } if get_config_value('unreleased.dynamic_context.enabled') is True: NODE_CLASS_MAPPINGS[RgthreeDynamicContext.NAME] = RgthreeDynamicContext NODE_CLASS_MAPPINGS[RgthreeDynamicContextSwitch.NAME] = RgthreeDynamicContextSwitch # WEB_DIRECTORY is the comfyui nodes directory that ComfyUI will link and auto-load. WEB_DIRECTORY = "./web/comfyui" THIS_DIR = os.path.dirname(os.path.abspath(__file__)) DIR_WEB = os.path.abspath(f'{THIS_DIR}/{WEB_DIRECTORY}') DIR_PY = os.path.abspath(f'{THIS_DIR}/py') # remove old directories OLD_DIRS = [ os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree'), os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree-comfy'), ] for old_dir in OLD_DIRS: if os.path.exists(old_dir): shutil.rmtree(old_dir) __all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY'] NOT_NODES = ['constants', 'log', 'utils', 'rgthree', 'rgthree_server', 'image_clipbaord', 'config'] nodes = [] for file in glob(os.path.join(DIR_PY, '*.py')) + glob(os.path.join(DIR_WEB, '*.js')): name = os.path.splitext(os.path.basename(file))[0] if name in NOT_NODES or name in nodes: continue if name.startswith('_') or name.startswith('base') or 'utils' in name: continue nodes.append(name) if name == 'display_any': nodes.append('display_int') print() adjs = ['exciting', 'extraordinary', 'epic', 'fantastic', 'magnificent'] log(f'Loaded {len(nodes)} {random.choice(adjs)} nodes.', color='BRIGHT_GREEN') # Alright, I don't like doing this, but until https://github.com/comfyanonymous/ComfyUI/issues/1502 # and/or https://github.com/comfyanonymous/ComfyUI/pull/1503 is pulled into ComfyUI, we need a way # to optimize the recursion that happens on prompt eval. This is particularly important for # rgthree nodes because workflows can contain many context nodes, but the problem would exist for # other nodes' (like "pipe" nodes, efficieny nodes). With `Context Big` nodes being # introduced, the number of input recursion that happens in these methods is exponential with a # saving of 1000's of percentage points over. # We'll use this to check if we _can_ patch execution. Other work to change the execution may # remove these methods, and we want to ensure people's apps do not break. could_patch_execution = (hasattr(execution, 'recursive_output_delete_if_changed') and hasattr(execution, 'recursive_will_execute') and hasattr(execution.PromptExecutor, 'execute')) if get_config_value('features.patch_recursive_execution') is True: if not could_patch_execution: log("NOTE: Will NOT use rgthree's optimized recursive execution as ComfyUI has changed.", color='YELLOW') else: log("Will use rgthree's optimized recursive execution.", color='BRIGHT_GREEN') class RgthreePatchRecursiveExecute_Set_patch_recursive_execution_to_false_if_not_working: """A fake 'list' that the caller for recursive_will_execute expects but we override such that `len(inst)` will return the count number, and `inst[-1]` will return the unique_id. Since that all the caller cares about, we can save several minutes and many MB of ram by simply counting numbers instead of concatenating a list of millions (only to count it). However the caller expects such a list, so we fake it with this. This mimics the enhancement from https://github.com/rgthree/ComfyUI/commit/50b3fb1 but without modifying the execution.py """ def __init__(self, unique_id): self.unique_id = unique_id self.count = 0 def add(self, value): self.count += value def __getitem__(self, key): """Returns the `unique_id` with '-1' since that's what the caller expects.""" if key == -1: return self.unique_id # This one would future proof the proposed changes, in that case "0" is the count if key == 0: return self.count else: return -1 def __len__(self): """Returns the "count" of the "list" as if we were building up a list instea of just incrementing `count`. """ return self.count # The following (hopefully) future proofs if https://github.com/rgthree/ComfyUI/commit/50b3fb1 # goes in, which changes from using `len` on a list, to sort directly (and, thus "<" and ">"). def __gt__(self, other): return self.count > other def __lt__(self, other): return self.count < other def __str__(self): return str(( self.count, self.unique_id, )) # Caches which will be cleared on each run execution.rgthree_cache_recursive_output_delete_if_changed_output = {} execution.rgthree_cache_recursive_will_execute = {} execution.rgthree_is_currently_optimized = False def rgthree_execute(self, *args, **kwargs): """ A patch of ComfyUI's default execution for optimization (or un-optimization) via config.""" if get_config_value('features.patch_recursive_execution') is True: if could_patch_execution: log("Using rgthree's optimized recursive execution.", color='GREEN') # When we execute, we'll reset our global cache here. execution.rgthree_cache_recursive_output_delete_if_changed_output = {} execution.rgthree_cache_recursive_will_execute = {} if not execution.rgthree_is_currently_optimized: log("First run patching recursive_output_delete_if_changed and recursive_will_execute.", color='GREEN', msg_color='RESET') log( "Note: \33[0mIf execution seems broken due to forward ComfyUI changes, you can disable " + "the optimization from rgthree settings in ComfyUI.", color='YELLOW') execution.rgthree_old_recursive_output_delete_if_changed = execution.recursive_output_delete_if_changed execution.recursive_output_delete_if_changed = rgthree_recursive_output_delete_if_changed execution.rgthree_old_recursive_will_execute = execution.recursive_will_execute execution.recursive_will_execute = rgthree_recursive_will_execute execution.rgthree_is_currently_optimized = True elif execution.rgthree_is_currently_optimized: log("Removing optimizations to recursive_output_delete_if_changed and recursive_will_execute.", color='YELLOW', msg_color='RESET') log("You can enable optimization in the rgthree settings in ComfyUI.", color='CYAN') execution.recursive_output_delete_if_changed = execution.rgthree_old_recursive_output_delete_if_changed execution.recursive_will_execute = execution.rgthree_old_recursive_will_execute execution.rgthree_is_currently_optimized = False # We always call the original execute, it's just whether we patch or unpacth first. return self.rgthree_old_execute(*args, **kwargs) # We always patch execute, so we can check if we want to do work. Up in rgthree_execute we will # either patch or unpatch recursive_will_execute recursive_output_delete_if_changed at runtime when # config changes. execution.PromptExecutor.rgthree_old_execute = execution.PromptExecutor.execute execution.PromptExecutor.execute = rgthree_execute def rgthree_recursive_will_execute(prompt, outputs, current_item, *args, **kwargs): """Patches recursive_will_execute function to cache the result of each output.""" unique_id = current_item inputs = prompt[unique_id]['inputs'] will_execute = RgthreePatchRecursiveExecute_Set_patch_recursive_execution_to_false_if_not_working( unique_id) if unique_id in outputs: return will_execute will_execute.add(1) for x in inputs: input_data = inputs[x] if isinstance(input_data, list): input_unique_id = input_data[0] output_index = input_data[1] node_output_cache_key = f'{input_unique_id}.{output_index}' will_execute_value = None # If this node's output has already been recursively evaluated, then we can reuse. if node_output_cache_key in execution.rgthree_cache_recursive_will_execute: will_execute_value = execution.rgthree_cache_recursive_will_execute[node_output_cache_key] elif input_unique_id not in outputs: will_execute_value = execution.recursive_will_execute(prompt, outputs, input_unique_id, *args, **kwargs) execution.rgthree_cache_recursive_will_execute[node_output_cache_key] = will_execute_value if will_execute_value is not None: will_execute.add(len(will_execute_value)) return will_execute def rgthree_recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item, *args, **kwargs): """Patches recursive_output_delete_if_changed function to cache the result of each output.""" unique_id = current_item inputs = prompt[unique_id]['inputs'] class_type = prompt[unique_id]['class_type'] class_def = execution.nodes.NODE_CLASS_MAPPINGS[class_type] is_changed_old = '' is_changed = '' to_delete = False if hasattr(class_def, 'IS_CHANGED'): if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]: is_changed_old = old_prompt[unique_id]['is_changed'] if 'is_changed' not in prompt[unique_id]: input_data_all = execution.get_input_data(inputs, class_def, unique_id, outputs) if input_data_all is not None: try: #is_changed = class_def.IS_CHANGED(**input_data_all) is_changed = execution.map_node_over_list(class_def, input_data_all, "IS_CHANGED") prompt[unique_id]['is_changed'] = is_changed except: to_delete = True else: is_changed = prompt[unique_id]['is_changed'] if unique_id not in outputs: return True if not to_delete: if is_changed != is_changed_old: to_delete = True elif unique_id not in old_prompt: to_delete = True elif inputs == old_prompt[unique_id]['inputs']: for x in inputs: input_data = inputs[x] if isinstance(input_data, list): input_unique_id = input_data[0] output_index = input_data[1] node_output_cache_key = f'{input_unique_id}.{output_index}' # If this node's output has already been recursively evaluated, then we can stop. if node_output_cache_key in execution.rgthree_cache_recursive_output_delete_if_changed_output: to_delete = execution.rgthree_cache_recursive_output_delete_if_changed_output[ node_output_cache_key] elif input_unique_id in outputs: to_delete = execution.recursive_output_delete_if_changed(prompt, old_prompt, outputs, input_unique_id, *args, **kwargs) execution.rgthree_cache_recursive_output_delete_if_changed_output[ node_output_cache_key] = to_delete else: to_delete = True if to_delete: break else: to_delete = True if to_delete: d = outputs.pop(unique_id) del d return to_delete print()