diff --git a/invokeai/app/invocations/__init__.py b/invokeai/app/invocations/__init__.py index 91a2edc680..32cf73d215 100644 --- a/invokeai/app/invocations/__init__.py +++ b/invokeai/app/invocations/__init__.py @@ -7,10 +7,13 @@ from invokeai.app.services.config.config_default import InvokeAIAppConfig custom_nodes_path = Path(InvokeAIAppConfig.get_config().custom_nodes_path.absolute()) custom_nodes_path.mkdir(parents=True, exist_ok=True) + custom_nodes_init_path = str(custom_nodes_path / "__init__.py") +custom_nodes_readme_path = str(custom_nodes_path / "README.md") # copy our custom nodes __init__.py to the custom nodes directory -shutil.copy(Path(__file__).parent / "_custom_nodes_init.py", custom_nodes_init_path) +shutil.copy(Path(__file__).parent / "custom_nodes/init.py", custom_nodes_init_path) +shutil.copy(Path(__file__).parent / "custom_nodes/README.md", custom_nodes_readme_path) # Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically spec = spec_from_file_location("custom_nodes", custom_nodes_init_path) @@ -21,5 +24,5 @@ sys.modules[spec.name] = module spec.loader.exec_module(module) # add core nodes to __all__ -python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.rglob("*.py")) +python_files = filter(lambda f: not f.name.startswith("_"), Path(__file__).parent.glob("*.py")) __all__ = list(f.stem for f in python_files) # type: ignore diff --git a/invokeai/app/invocations/_custom_nodes_init.py b/invokeai/app/invocations/_custom_nodes_init.py deleted file mode 100644 index 561f6de382..0000000000 --- a/invokeai/app/invocations/_custom_nodes_init.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -InvokeAI custom nodes initialization - -This file is responsible for loading all custom nodes from this directory. - -All python files are loaded on app startup. Custom nodes will be initialized and available for use -in workflows. - -The app must be restarted for changes to be picked up. - -This file is overwritten on launch. Do not edit this file directly. -""" -import sys -from importlib import import_module -from importlib.util import module_from_spec, spec_from_file_location -from pathlib import Path - -from invokeai.backend.util.logging import InvokeAILogger - -logger = InvokeAILogger.get_logger() -count = 0 -for f in Path(__file__).parent.rglob("*.py"): - module_name = f.stem - if (not module_name.startswith("_")) and (module_name not in globals()): - spec = spec_from_file_location(module_name, f.absolute()) - if spec is None or spec.loader is None: - logger.warn(f"Could not load {f}") - continue - module = module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - count += 1 - del f, module_name - -logger.info(f"Loaded {count} modules from {Path(__file__).parent}") - -del import_module, Path diff --git a/invokeai/app/invocations/custom_nodes/README.md b/invokeai/app/invocations/custom_nodes/README.md new file mode 100644 index 0000000000..d93bb65539 --- /dev/null +++ b/invokeai/app/invocations/custom_nodes/README.md @@ -0,0 +1,51 @@ +# Custom Nodes / Node Packs + +Copy your node packs to this directory. + +When nodes are added or changed, you must restart the app to see the changes. + +## Directory Structure + +For a node pack to be loaded, it must be placed in a directory alongside this +file. Here's an example structure: + +```py +. +├── __init__.py # Invoke-managed custom node loader +│ +├── cool_node +│ ├── __init__.py # see example below +│ └── cool_node.py +│ +└── my_node_pack + ├── __init__.py # see example below + ├── tasty_node.py + ├── bodacious_node.py + ├── utils.py + └── extra_nodes + └── fancy_node.py +``` + +## Node Pack `__init__.py` + +Each node pack must have an `__init__.py` file that imports its nodes. + +The structure of each node or node pack is otherwise not important. + +Here are examples, based on the example directory structure. + +### `cool_node/__init__.py` + +```py +from .cool_node import CoolInvocation +``` + +### `my_node_pack/__init__.py` + +```py +from .tasty_node import TastyInvocation +from .bodacious_node import BodaciousInvocation +from .extra_nodes.fancy_node import FancyInvocation +``` + +Only nodes imported in the `__init__.py` file are loaded. diff --git a/invokeai/app/invocations/custom_nodes/init.py b/invokeai/app/invocations/custom_nodes/init.py new file mode 100644 index 0000000000..c6708e95a7 --- /dev/null +++ b/invokeai/app/invocations/custom_nodes/init.py @@ -0,0 +1,51 @@ +""" +Invoke-managed custom node loader. See README.md for more information. +""" + +import sys +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger() +loaded_count = 0 + + +for d in Path(__file__).parent.iterdir(): + # skip files + if not d.is_dir(): + continue + + # skip hidden directories + if d.name.startswith("_") or d.name.startswith("."): + continue + + # skip directories without an `__init__.py` + init = d / "__init__.py" + if not init.exists(): + continue + + module_name = init.parent.stem + + # skip if already imported + if module_name in globals(): + continue + + # we have a legit module to import + spec = spec_from_file_location(module_name, init.absolute()) + + if spec is None or spec.loader is None: + logger.warn(f"Could not load {init}") + continue + + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + + loaded_count += 1 + + del init, module_name + + +logger.info(f"Loaded {loaded_count} modules from {Path(__file__).parent}")