diff --git a/custom_nodes/example_folder/main.py b/custom_nodes/example_folder/main.py deleted file mode 100644 index d906a558..00000000 --- a/custom_nodes/example_folder/main.py +++ /dev/null @@ -1,87 +0,0 @@ -from utils import waste_cpu_resource -class ExampleFolder: - """ - A example node - - Class methods - ------------- - INPUT_TYPES (dict): - Tell the main program input parameters of nodes. - - Attributes - ---------- - RETURN_TYPES (`tuple`): - The type of each element in the output tulple. - FUNCTION (`str`): - The name of the entry-point method which will return a tuple. For example, if `FUNCTION = "execute"` then it will run Example().execute() - OUTPUT_NODE ([`bool`]): - WIP - CATEGORY (`str`): - WIP - 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. - The type can be a string indicate a type or a list indicate selection. - Prebuilt types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT". - Input in type "INT", "STRING" or "FLOAT" will be converted automatically from a string to the corresponse Python type before passing and have special config - Argument: s (`None`): Useless ig - 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. - + Secound value is a config for type "INT", "STRING" or "FLOAT". - """ - return { - "required": { - "string_field": ("STRING", { - "multiline": True, #Allow the input to be multilined - "default": "Hello World!" - }), - "int_field": ("INT", { - "default": 0, - "min": 0, #Minimum value - "max": 4096, #Maximum value - "step": 64 #Slider's step - }), - #Like INT - "print_to_screen": (["Enable", "Disable"], {"default": "Enable"}) - }, - #"hidden": { - # "prompt": "PROMPT", - # "extra_pnginfo": "EXTRA_PNGINFO" - #}, - } - - RETURN_TYPES = ("STRING", "INT", "FLOAT", "STRING") - FUNCTION = "test" - - #OUTPUT_NODE = True - - CATEGORY = "Example" - - def test(self, string_field, int_field, print_to_screen): - rand_float = waste_cpu_resource() - if print_to_screen == "Enable": - print(f"""Your input contains: - string_field aka input text: {string_field} - int_field: {int_field} - A random float number: {rand_float} - """) - return (string_field, int_field, rand_float, print_to_screen) - -NODE_CLASS_MAPPINGS = { - "ExampleFolder": ExampleFolder -} -""" -NODE_CLASS_MAPPINGS (dict): A dictionary contains all nodes you want to export -""" \ No newline at end of file diff --git a/custom_nodes/example_folder/utils.py b/custom_nodes/example_folder/utils.py deleted file mode 100644 index cc59f97f..00000000 --- a/custom_nodes/example_folder/utils.py +++ /dev/null @@ -1,4 +0,0 @@ -import torch -def waste_cpu_resource(): - x = torch.rand(1, 1e6, dtype=torch.float64).cpu() - return x.numpy()[0, 1] \ No newline at end of file diff --git a/custom_nodes/example.py b/custom_nodes/example_node.py.example similarity index 61% rename from custom_nodes/example.py rename to custom_nodes/example_node.py.example index ff3a46bc..1bb1a5a3 100644 --- a/custom_nodes/example.py +++ b/custom_nodes/example_node.py.example @@ -12,11 +12,13 @@ class Example: RETURN_TYPES (`tuple`): The type of each element in the output tulple. FUNCTION (`str`): - The name of the entry-point method which will return a tuple. For example, if `FUNCTION = "execute"` then it will run Example().execute() + The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute() OUTPUT_NODE ([`bool`]): - WIP + 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`): - WIP + The category the node should appear in the UI. 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`. @@ -28,10 +30,10 @@ class Example: def INPUT_TYPES(s): """ Return a dictionary which contains config for all input fields. - The type can be a string indicate a type or a list indicate selection. - Prebuilt types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT". - Input in type "INT", "STRING" or "FLOAT" will be converted automatically from a string to the corresponse Python type before passing and have special config - Argument: s (`None`): Useless ig + 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: @@ -42,46 +44,43 @@ class Example: """ return { "required": { - "string_field": ("STRING", { - "multiline": True, #Allow the input to be multilined - "default": "Hello World!" - }), + "image": ("IMAGE",), "int_field": ("INT", { "default": 0, "min": 0, #Minimum value "max": 4096, #Maximum value "step": 64 #Slider's step }), - #Like INT "float_field": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), - "print_to_screen": (["Enable", "Disable"], {"default": "Enable"}) + "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!" + }), }, - #"hidden": { - # "prompt": "PROMPT", - # "extra_pnginfo": "EXTRA_PNGINFO" - #}, } - RETURN_TYPES = ("STRING", "INT", "FLOAT", "STRING") + RETURN_TYPES = ("IMAGE",) FUNCTION = "test" - #OUTPUT_NODE = True + #OUTPUT_NODE = False CATEGORY = "Example" - def test(self, string_field, int_field, float_field, print_to_screen): - if print_to_screen == "Enable": + 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} """) - return (string_field, int_field, float_field, print_to_screen) + #do some processing on the image, in this example I just invert it + image = 1.0 - image + return (image,) +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique NODE_CLASS_MAPPINGS = { "Example": Example } -""" -NODE_CLASS_MAPPINGS (dict): A dictionary contains all nodes you want to export -""" \ No newline at end of file diff --git a/nodes.py b/nodes.py index d2fd3ff4..ec65ca3a 100644 --- a/nodes.py +++ b/nodes.py @@ -5,6 +5,7 @@ import sys import json import hashlib import copy +import traceback from PIL import Image from PIL.PngImagePlugin import PngInfo @@ -751,29 +752,28 @@ NODE_CLASS_MAPPINGS = { CUSTOM_NODE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "custom_nodes") def load_custom_nodes(): possible_modules = os.listdir(CUSTOM_NODE_PATH) - try: - #Comment out these two lines if you want to test - possible_modules.remove("example.py") - possible_modules.remove("example_folder") + if "__pycache__" in possible_modules: possible_modules.remove("__pycache__") - except ValueError: pass + for possible_module in possible_modules: module_path = os.path.join(CUSTOM_NODE_PATH, possible_module) if os.path.isfile(module_path) and os.path.splitext(module_path)[1] != ".py": continue - + + module_name = "custom_node_module.{}".format(possible_module) try: if os.path.isfile(module_path): - module_spec = importlib.util.spec_from_file_location(os.path.basename(module_path), module_path) + module_spec = importlib.util.spec_from_file_location(module_name, module_path) else: - module_spec = importlib.util.spec_from_file_location(module_path, "main.py") + module_spec = importlib.util.spec_from_file_location(module_name, os.path.join(module_path, "__init__.py")) module = importlib.util.module_from_spec(module_spec) + sys.modules[module_name] = module module_spec.loader.exec_module(module) - if getattr(module, "NODE_CLASS_MAPPINGS") is not None: + if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None: NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS) else: print(f"Skip {possible_module} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS.") - except ImportError as e: - print(f"Cannot import {possible_module} module for custom nodes.") - print(e) + except Exception as e: + print(traceback.format_exc()) + print(f"Cannot import {possible_module} module for custom nodes:", e) load_custom_nodes() \ No newline at end of file