diff --git a/comfy/cli_args.py b/comfy/cli_args.py index 7234a7ba0975..d66ba33f7ea2 100644 --- a/comfy/cli_args.py +++ b/comfy/cli_args.py @@ -119,6 +119,12 @@ class LatentPreviewMethod(enum.Enum): upcast.add_argument("--dont-upcast-attention", action="store_true", help="Disable all upcasting of attention. Should be unnecessary except for debugging.") +manager_group = parser.add_mutually_exclusive_group() +manager_group.add_argument("--disable-manager", action="store_true", help="Completely disable the ComfyUI-Manager feature.") +manager_group.add_argument("--disable-manager-ui", action="store_true", help="Disables only the ComfyUI-Manager UI and endpoints. Scheduled installations and similar background tasks will still operate.") +manager_group.add_argument("--enable-manager-legacy-ui", action="store_true", help="Enables the legacy UI of ComfyUI-Manager") + + vram_group = parser.add_mutually_exclusive_group() vram_group.add_argument("--gpu-only", action="store_true", help="Store and run everything (text encoders/CLIP models, etc... on the GPU).") vram_group.add_argument("--highvram", action="store_true", help="By default models will be unloaded to CPU memory after being used. This option keeps them in GPU memory.") @@ -159,6 +165,7 @@ class PerformanceFeature(enum.Enum): parser.add_argument("--verbose", default='INFO', const='DEBUG', nargs="?", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help='Set the logging level') parser.add_argument("--log-stdout", action="store_true", help="Send normal process output to stdout instead of stderr (default).") + # The default built-in provider hosted under web/ DEFAULT_VERSION_STRING = "comfyanonymous/ComfyUI@latest" diff --git a/custom_nodes/example_node.py.example b/custom_nodes/example_node.py.example deleted file mode 100644 index 29ab2aa72319..000000000000 --- a/custom_nodes/example_node.py.example +++ /dev/null @@ -1,155 +0,0 @@ -class Example: - """ - A example node - - Class methods - ------------- - INPUT_TYPES (dict): - Tell the main program input parameters of nodes. - IS_CHANGED: - optional method to control when the node is re executed. - - Attributes - ---------- - RETURN_TYPES (`tuple`): - The type of each element in the output tuple. - RETURN_NAMES (`tuple`): - Optional: The name of each output in the output tuple. - FUNCTION (`str`): - The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute() - OUTPUT_NODE ([`bool`]): - If this node is an output node that outputs a result/image from the graph. The SaveImage node is an example. - The backend iterates on these output nodes and tries to execute all their parents if their parent graph is properly connected. - Assumed to be False if not present. - CATEGORY (`str`): - The category the node should appear in the UI. - DEPRECATED (`bool`): - Indicates whether the node is deprecated. Deprecated nodes are hidden by default in the UI, but remain - functional in existing workflows that use them. - EXPERIMENTAL (`bool`): - Indicates whether the node is experimental. Experimental nodes are marked as such in the UI and may be subject to - significant changes or removal in future versions. Use with caution in production workflows. - execute(s) -> tuple || None: - The entry point method. The name of this method must be the same as the value of property `FUNCTION`. - For example, if `FUNCTION = "execute"` then this method's name must be `execute`, if `FUNCTION = "foo"` then it must be `foo`. - """ - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(s): - """ - Return a dictionary which contains config for all input fields. - Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT". - Input types "INT", "STRING" or "FLOAT" are special values for fields on the node. - The type can be a list for selection. - - Returns: `dict`: - - Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required` - - Value input_fields (`dict`): Contains input fields config: - * Key field_name (`string`): Name of a entry-point method's argument - * Value field_config (`tuple`): - + First value is a string indicate the type of field or a list for selection. - + Second value is a config for type "INT", "STRING" or "FLOAT". - """ - return { - "required": { - "image": ("IMAGE",), - "int_field": ("INT", { - "default": 0, - "min": 0, #Minimum value - "max": 4096, #Maximum value - "step": 64, #Slider's step - "display": "number", # Cosmetic only: display as "number" or "slider" - "lazy": True # Will only be evaluated if check_lazy_status requires it - }), - "float_field": ("FLOAT", { - "default": 1.0, - "min": 0.0, - "max": 10.0, - "step": 0.01, - "round": 0.001, #The value representing the precision to round to, will be set to the step value by default. Can be set to False to disable rounding. - "display": "number", - "lazy": True - }), - "print_to_screen": (["enable", "disable"],), - "string_field": ("STRING", { - "multiline": False, #True if you want the field to look like the one on the ClipTextEncode node - "default": "Hello World!", - "lazy": True - }), - }, - } - - RETURN_TYPES = ("IMAGE",) - #RETURN_NAMES = ("image_output_name",) - - FUNCTION = "test" - - #OUTPUT_NODE = False - - CATEGORY = "Example" - - def check_lazy_status(self, image, string_field, int_field, float_field, print_to_screen): - """ - Return a list of input names that need to be evaluated. - - This function will be called if there are any lazy inputs which have not yet been - evaluated. As long as you return at least one field which has not yet been evaluated - (and more exist), this function will be called again once the value of the requested - field is available. - - Any evaluated inputs will be passed as arguments to this function. Any unevaluated - inputs will have the value None. - """ - if print_to_screen == "enable": - return ["int_field", "float_field", "string_field"] - else: - return [] - - def test(self, image, string_field, int_field, float_field, print_to_screen): - if print_to_screen == "enable": - print(f"""Your input contains: - string_field aka input text: {string_field} - int_field: {int_field} - float_field: {float_field} - """) - #do some processing on the image, in this example I just invert it - image = 1.0 - image - return (image,) - - """ - The node will always be re executed if any of the inputs change but - this method can be used to force the node to execute again even when the inputs don't change. - You can make this node return a number or a string. This value will be compared to the one returned the last time the node was - executed, if it is different the node will be executed again. - This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash - changes between executions the LoadImage node is executed again. - """ - #@classmethod - #def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen): - # return "" - -# Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension -# WEB_DIRECTORY = "./somejs" - - -# Add custom API routes, using router -from aiohttp import web -from server import PromptServer - -@PromptServer.instance.routes.get("/hello") -async def get_hello(request): - return web.json_response("hello") - - -# A dictionary that contains all nodes you want to export with their names -# NOTE: names should be globally unique -NODE_CLASS_MAPPINGS = { - "Example": Example -} - -# A dictionary that contains the friendly/humanly readable titles for the nodes -NODE_DISPLAY_NAME_MAPPINGS = { - "Example": "Example Node" -} diff --git a/custom_nodes/websocket_image_save.py b/custom_nodes/websocket_image_save.py deleted file mode 100644 index 15f87f9f5617..000000000000 --- a/custom_nodes/websocket_image_save.py +++ /dev/null @@ -1,44 +0,0 @@ -from PIL import Image -import numpy as np -import comfy.utils -import time - -#You can use this node to save full size images through the websocket, the -#images will be sent in exactly the same format as the image previews: as -#binary images on the websocket with a 8 byte header indicating the type -#of binary message (first 4 bytes) and the image format (next 4 bytes). - -#Note that no metadata will be put in the images saved with this node. - -class SaveImageWebsocket: - @classmethod - def INPUT_TYPES(s): - return {"required": - {"images": ("IMAGE", ),} - } - - RETURN_TYPES = () - FUNCTION = "save_images" - - OUTPUT_NODE = True - - CATEGORY = "api/image" - - def save_images(self, images): - pbar = comfy.utils.ProgressBar(images.shape[0]) - step = 0 - for image in images: - i = 255. * image.cpu().numpy() - img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - pbar.update_absolute(step, images.shape[0], ("PNG", img, None)) - step += 1 - - return {} - - @classmethod - def IS_CHANGED(s, images): - return time.time() - -NODE_CLASS_MAPPINGS = { - "SaveImageWebsocket": SaveImageWebsocket, -} diff --git a/main.py b/main.py index d488c0f4c12b..24344cf1ae2d 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,9 @@ import logging import sys +if not args.disable_manager: + import comfyui_manager + if __name__ == "__main__": #NOTE: These do not do anything on core ComfyUI, they are for custom nodes. os.environ['HF_HUB_DISABLE_TELEMETRY'] = '1' @@ -76,6 +79,11 @@ def execute_script(script_path): for possible_module in possible_modules: module_path = os.path.join(custom_node_path, possible_module) + + if not args.disable_manager: + if comfyui_manager.should_be_disabled(module_path): + continue + if os.path.isfile(module_path) or module_path.endswith(".disabled") or module_path == "__pycache__": continue @@ -98,6 +106,10 @@ def execute_script(script_path): logging.info("") apply_custom_paths() + +if not args.disable_manager: + comfyui_manager.prestartup() + execute_prestartup_script() @@ -278,6 +290,9 @@ def start_comfyui(asyncio_loop=None): asyncio.set_event_loop(asyncio_loop) prompt_server = server.PromptServer(asyncio_loop) + if not args.disable_manager and not args.disable_manager_ui: + comfyui_manager.start() + hook_breaker_ac10a0.save_functions() nodes.init_extra_nodes( init_custom_nodes=(not args.disable_all_custom_nodes) or len(args.whitelist_custom_nodes) > 0, diff --git a/nodes.py b/nodes.py index 1b465b9e6e19..5aba1e9092cf 100644 --- a/nodes.py +++ b/nodes.py @@ -38,6 +38,9 @@ import latent_preview import node_helpers +if not args.disable_manager: + import comfyui_manager + def before_node_execution(): comfy.model_management.throw_exception_if_processing_interrupted() @@ -2190,6 +2193,12 @@ def init_external_custom_nodes(): if args.disable_all_custom_nodes and possible_module not in args.whitelist_custom_nodes: logging.info(f"Skipping {possible_module} due to disable_all_custom_nodes and whitelist_custom_nodes") continue + + if not args.disable_manager: + if comfyui_manager.should_be_disabled(module_path): + logging.info(f"Blocked by policy: {module_path}") + continue + time_before = time.perf_counter() success = load_custom_node(module_path, base_node_names, module_parent="custom_nodes") node_import_times.append((time.perf_counter() - time_before, module_path, success)) diff --git a/requirements.txt b/requirements.txt index 479a29eecd84..6db58fb15058 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ comfyui-frontend-package==1.23.4 comfyui-workflow-templates==0.1.31 comfyui-embedded-docs==0.2.3 +comfyui_manager torch torchsde torchvision diff --git a/server.py b/server.py index 878b5eeb1eb5..993d52daf0b3 100644 --- a/server.py +++ b/server.py @@ -36,6 +36,9 @@ from typing import Optional, Union from api_server.routes.internal.internal_routes import InternalRoutes +if not args.disable_manager: + import comfyui_manager + class BinaryEventTypes: PREVIEW_IMAGE = 1 UNENCODED_PREVIEW_IMAGE = 2 @@ -175,6 +178,9 @@ def __init__(self, loop): else: middlewares.append(create_origin_only_middleware()) + if not args.disable_manager: + middlewares.append(comfyui_manager.create_middleware()) + max_upload_size = round(args.max_upload_size * 1024 * 1024) self.app = web.Application(client_max_size=max_upload_size, middlewares=middlewares) self.sockets = dict()