From 14c07fd7341ff2f813f55f99a2dfddcc37582ae1 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 11 Oct 2025 15:29:29 -0700 Subject: [PATCH] [refactor] reorganize devtools test nodes into modules (#6020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Refactored monolithic devtools node definitions into organized module structure for better maintainability and separation of concerns. ## Changes - **What**: Split 700+ line `dev_nodes.py` into modular structure under `tools/devtools/nodes/` with categorized files: `errors.py`, `inputs.py`, `models.py`, `remote.py` - **Dependencies**: None ## Review Focus Module import structure and ensure all node registrations are properly preserved in the consolidated mappings. **Before:** ``` tools/devtools/ ├── __init__.py └── dev_nodes.py (738 lines) ``` **After:** ``` tools/devtools/ ├── __init__.py ├── dev_nodes.py (65 lines - imports only) └── nodes/ ├── __init__.py (consolidated mappings) ├── errors.py (error/debug nodes) ├── inputs.py (input/widget nodes) ├── models.py (model/patch nodes) └── remote.py (remote/combo nodes) ``` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6020-refactor-reorganize-devtools-test-nodes-into-modules-2896d73d365081e89efef7e88ca8fee3) by [Unito](https://www.unito.io) --- tools/devtools/__init__.py | 2 +- tools/devtools/dev_nodes.py | 740 +++---------------------------- tools/devtools/nodes/__init__.py | 93 ++++ tools/devtools/nodes/errors.py | 89 ++++ tools/devtools/nodes/inputs.py | 357 +++++++++++++++ tools/devtools/nodes/models.py | 84 ++++ tools/devtools/nodes/remote.py | 220 +++++++++ 7 files changed, 911 insertions(+), 674 deletions(-) create mode 100644 tools/devtools/nodes/__init__.py create mode 100644 tools/devtools/nodes/errors.py create mode 100644 tools/devtools/nodes/inputs.py create mode 100644 tools/devtools/nodes/models.py create mode 100644 tools/devtools/nodes/remote.py diff --git a/tools/devtools/__init__.py b/tools/devtools/__init__.py index 3ac47cb18..f9b457896 100644 --- a/tools/devtools/__init__.py +++ b/tools/devtools/__init__.py @@ -97,4 +97,4 @@ async def set_settings(request: Request): return web.Response(status=500, text=f"Error: {str(e)}") -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] \ No newline at end of file +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/tools/devtools/dev_nodes.py b/tools/devtools/dev_nodes.py index 15311d74b..660518d84 100644 --- a/tools/devtools/dev_nodes.py +++ b/tools/devtools/dev_nodes.py @@ -1,673 +1,67 @@ -import torch -import comfy.utils as utils -from comfy.model_patcher import ModelPatcher -import nodes -import time -import os -import folder_paths - - -class ErrorRaiseNode: - @classmethod - def INPUT_TYPES(cls): - return {"required": {}} - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "raise_error" - CATEGORY = "DevTools" - DESCRIPTION = "Raise an error for development purposes" - - def raise_error(self): - raise Exception("Error node was called!") - - -class ErrorRaiseNodeWithMessage: - @classmethod - def INPUT_TYPES(cls): - return {"required": {"message": ("STRING", {"multiline": True})}} - - RETURN_TYPES = () - OUTPUT_NODE = True - - FUNCTION = "raise_error" - CATEGORY = "DevTools" - DESCRIPTION = "Raise an error with message for development purposes" - - def raise_error(self, message: str): - raise Exception(message) - - -class ExperimentalNode: - @classmethod - def INPUT_TYPES(cls): - return {"required": {}} - - RETURN_TYPES = () - OUTPUT_NODE = True - FUNCTION = "experimental_function" - CATEGORY = "DevTools" - DESCRIPTION = "A experimental node" - - EXPERIMENTAL = True - - def experimental_function(self): - print("Experimental node was called!") - - -class DeprecatedNode: - @classmethod - def INPUT_TYPES(cls): - return {"required": {}} - - RETURN_TYPES = () - OUTPUT_NODE = True - FUNCTION = "deprecated_function" - CATEGORY = "DevTools" - DESCRIPTION = "A deprecated node" - - DEPRECATED = True - - def deprecated_function(self): - print("Deprecated node was called!") - - -class LongComboDropdown: - @classmethod - def INPUT_TYPES(cls): - return {"required": {"option": ([f"Option {i}" for i in range(1_000)],)}} - - RETURN_TYPES = () - OUTPUT_NODE = True - FUNCTION = "long_combo_dropdown" - CATEGORY = "DevTools" - DESCRIPTION = "A long combo dropdown" - - def long_combo_dropdown(self, option: str): - print(option) - - -class NodeWithOptionalInput: - @classmethod - def INPUT_TYPES(cls): - return { - "required": {"required_input": ("IMAGE",)}, - "optional": {"optional_input": ("IMAGE", {"default": None})}, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "node_with_optional_input" - CATEGORY = "DevTools" - DESCRIPTION = "A node with an optional input" - - def node_with_optional_input(self, required_input, optional_input=None): - print( - f"Calling node with required_input: {required_input} and optional_input: {optional_input}" - ) - return (required_input,) - - -class NodeWithOptionalComboInput: - @classmethod - def INPUT_TYPES(cls): - return { - "optional": { - "optional_combo_input": ( - [f"Random Unique Option {time.time()}" for _ in range(8)], - {"default": None}, - ) - }, - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "node_with_optional_combo_input" - CATEGORY = "DevTools" - DESCRIPTION = "A node with an optional combo input that returns unique values every time INPUT_TYPES is called" - - def node_with_optional_combo_input(self, optional_combo_input=None): - print(f"Calling node with optional_combo_input: {optional_combo_input}") - return (optional_combo_input,) - - -class NodeWithOnlyOptionalInput: - @classmethod - def INPUT_TYPES(s): - return { - "optional": { - "text": ("STRING", {"multiline": True, "dynamicPrompts": True}), - "clip": ("CLIP", {}), - } - } - - RETURN_TYPES = () - FUNCTION = "node_with_only_optional_input" - CATEGORY = "DevTools" - DESCRIPTION = "A node with only optional input" - - def node_with_only_optional_input(self, clip=None, text=None): - pass - - -class NodeWithOutputList: - @classmethod - def INPUT_TYPES(cls): - return {"required": {}} - - RETURN_TYPES = ( - "INT", - "INT", - ) - RETURN_NAMES = ( - "INTEGER OUTPUT", - "INTEGER LIST OUTPUT", - ) - OUTPUT_IS_LIST = ( - False, - True, - ) - FUNCTION = "node_with_output_list" - CATEGORY = "DevTools" - DESCRIPTION = "A node with an output list" - - def node_with_output_list(self): - return (1, [1, 2, 3]) - - -class NodeWithForceInput: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "int_input": ("INT", {"forceInput": True}), - "int_input_widget": ("INT", {"default": 1}), - }, - "optional": {"float_input": ("FLOAT", {"forceInput": True})}, - } - - RETURN_TYPES = () - OUTPUT_NODE = True - FUNCTION = "node_with_force_input" - CATEGORY = "DevTools" - DESCRIPTION = "A node with a forced input" - - def node_with_force_input( - self, int_input: int, int_input_widget: int, float_input: float = 0.0 - ): - print( - f"int_input: {int_input}, int_input_widget: {int_input_widget}, float_input: {float_input}" - ) - - -class NodeWithDefaultInput: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "int_input": ("INT", {"defaultInput": True}), - "int_input_widget": ("INT", {"default": 1}), - }, - "optional": {"float_input": ("FLOAT", {"defaultInput": True})}, - } - - RETURN_TYPES = () - OUTPUT_NODE = True - FUNCTION = "node_with_default_input" - CATEGORY = "DevTools" - DESCRIPTION = "A node with a default input" - - def node_with_default_input( - self, int_input: int, int_input_widget: int, float_input: float = 0.0 - ): - print( - f"int_input: {int_input}, int_input_widget: {int_input_widget}, float_input: {float_input}" - ) - - -class NodeWithStringInput: - @classmethod - def INPUT_TYPES(cls): - return {"required": {"string_input": ("STRING",)}} - - RETURN_TYPES = () - FUNCTION = "node_with_string_input" - CATEGORY = "DevTools" - DESCRIPTION = "A node with a string input" - - def node_with_string_input(self, string_input: str): - print(f"string_input: {string_input}") - - -class NodeWithUnionInput: - @classmethod - def INPUT_TYPES(cls): - return { - "optional": { - "string_or_int_input": ("STRING,INT",), - "string_input": ("STRING", {"forceInput": True}), - "int_input": ("INT", {"forceInput": True}), - } - } - - RETURN_TYPES = () - OUTPUT_NODE = True - FUNCTION = "node_with_union_input" - CATEGORY = "DevTools" - DESCRIPTION = "A node with a union input" - - def node_with_union_input( - self, - string_or_int_input: str | int = "", - string_input: str = "", - int_input: int = 0, - ): - print( - f"string_or_int_input: {string_or_int_input}, string_input: {string_input}, int_input: {int_input}" - ) - return { - "ui": { - "text": string_or_int_input, - } - } - - -class NodeWithBooleanInput: - @classmethod - def INPUT_TYPES(cls): - return {"required": {"boolean_input": ("BOOLEAN",)}} - - RETURN_TYPES = () - FUNCTION = "node_with_boolean_input" - CATEGORY = "DevTools" - DESCRIPTION = "A node with a boolean input" - - def node_with_boolean_input(self, boolean_input: bool): - print(f"boolean_input: {boolean_input}") - - -class SimpleSlider: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": ( - "FLOAT", - { - "display": "slider", - "default": 0.5, - "min": 0.0, - "max": 1.0, - "step": 0.001, - }, - ), - }, - } - - RETURN_TYPES = ("FLOAT",) - FUNCTION = "execute" - CATEGORY = "DevTools" - - def execute(self, value): - return (value,) - - -class NodeWithSeedInput: - @classmethod - def INPUT_TYPES(cls): - return {"required": {"seed": ("INT", {"default": 0})}} - - RETURN_TYPES = () - FUNCTION = "node_with_seed_input" - CATEGORY = "DevTools" - DESCRIPTION = "A node with a seed input" - OUTPUT_NODE = True - - def node_with_seed_input(self, seed: int): - print(f"seed: {seed}") - - -class DummyPatch(torch.nn.Module): - def __init__(self, module: torch.nn.Module, dummy_float: float = 0.0): - super().__init__() - self.module = module - self.dummy_float = dummy_float - - def forward(self, *args, **kwargs): - if isinstance(self.module, DummyPatch): - raise Exception(f"Calling nested dummy patch! {self.dummy_float}") - - return self.module(*args, **kwargs) - - -class ObjectPatchNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "model": ("MODEL",), - "target_module": ("STRING", {"multiline": True}), - }, - "optional": { - "dummy_float": ("FLOAT", {"default": 0.0}), - }, - } - - RETURN_TYPES = ("MODEL",) - FUNCTION = "apply_patch" - CATEGORY = "DevTools" - DESCRIPTION = "A node that applies an object patch" - - def apply_patch( - self, model: ModelPatcher, target_module: str, dummy_float: float = 0.0 - ) -> ModelPatcher: - module = utils.get_attr(model.model, target_module) - work_model = model.clone() - work_model.add_object_patch(target_module, DummyPatch(module, dummy_float)) - return (work_model,) - - -class RemoteWidgetNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "remote_widget_value": ( - "COMBO", - { - "remote": { - "route": "/api/models/checkpoints", - }, - }, - ), - }, - } - - FUNCTION = "remote_widget" - CATEGORY = "DevTools" - DESCRIPTION = "A node that lazily fetches options from a remote endpoint" - RETURN_TYPES = ("STRING",) - - def remote_widget(self, remote_widget_value: str): - return (remote_widget_value,) - - -class RemoteWidgetNodeWithParams: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "remote_widget_value": ( - "COMBO", - { - "remote": { - "route": "/api/models/checkpoints", - "query_params": { - "sort": "true", - }, - }, - }, - ), - }, - } - - FUNCTION = "remote_widget" - CATEGORY = "DevTools" - DESCRIPTION = ( - "A node that lazily fetches options from a remote endpoint with query params" - ) - RETURN_TYPES = ("STRING",) - - def remote_widget(self, remote_widget_value: str): - return (remote_widget_value,) - - -class RemoteWidgetNodeWithRefresh: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "remote_widget_value": ( - "COMBO", - { - "remote": { - "route": "/api/models/checkpoints", - "refresh": 300, - "max_retries": 10, - "timeout": 256, - }, - }, - ), - }, - } - - FUNCTION = "remote_widget" - CATEGORY = "DevTools" - DESCRIPTION = "A node that lazily fetches options from a remote endpoint and refresh the options every 300 ms" - RETURN_TYPES = ("STRING",) - - def remote_widget(self, remote_widget_value: str): - return (remote_widget_value,) - - -class RemoteWidgetNodeWithRefreshButton: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "remote_widget_value": ( - "COMBO", - { - "remote": { - "route": "/api/models/checkpoints", - "refresh_button": True, - }, - }, - ), - }, - } - - FUNCTION = "remote_widget" - CATEGORY = "DevTools" - DESCRIPTION = "A node that lazily fetches options from a remote endpoint and has a refresh button to manually reload options" - RETURN_TYPES = ("STRING",) - - def remote_widget(self, remote_widget_value: str): - return (remote_widget_value,) - - -class RemoteWidgetNodeWithControlAfterRefresh: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "remote_widget_value": ( - "COMBO", - { - "remote": { - "route": "/api/models/checkpoints", - "refresh_button": True, - "control_after_refresh": "first", - }, - }, - ), - }, - } - - FUNCTION = "remote_widget" - CATEGORY = "DevTools" - DESCRIPTION = "A node that lazily fetches options from a remote endpoint and has a refresh button to manually reload options and select the first option on refresh" - RETURN_TYPES = ("STRING",) - - def remote_widget(self, remote_widget_value: str): - return (remote_widget_value,) - - -class NodeWithOutputCombo: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "subset_options": (["A", "B"], {"forceInput": True}), - "subset_options_v2": ( - "COMBO", - {"options": ["A", "B"], "forceInput": True}, - ), - } - } - - RETURN_TYPES = (["A", "B", "C"],) - FUNCTION = "node_with_output_combo" - CATEGORY = "DevTools" - DESCRIPTION = "A node that outputs a combo type" - - def node_with_output_combo(self, subset_options: str): - return (subset_options,) - - -class MultiSelectNode: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "foo": ( - "COMBO", - { - "options": ["A", "B", "C"], - "multi_select": { - "placeholder": "Choose foos", - "chip": True, - }, - }, - ) - } - } - - RETURN_TYPES = ("STRING",) - OUTPUT_IS_LIST = [True] - FUNCTION = "multi_select_node" - CATEGORY = "DevTools" - DESCRIPTION = "A node that outputs a multi select type" - - def multi_select_node(self, foo: list[str]) -> list[str]: - return (foo,) - - -class LoadAnimatedImageTest(nodes.LoadImage): - @classmethod - def INPUT_TYPES(s): - input_dir = folder_paths.get_input_directory() - files = [ - f - for f in os.listdir(input_dir) - if os.path.isfile(os.path.join(input_dir, f)) and f.endswith(".webp") - ] - files = folder_paths.filter_files_content_types(files, ["image"]) - return { - "required": {"image": (sorted(files), {"animated_image_upload": True})}, - } - - -class NodeWithValidation: - @classmethod - def INPUT_TYPES(cls): - return { - "required": {"int_input": ("INT",)}, - } - - @classmethod - def VALIDATE_INPUTS(cls, int_input: int): - if int_input < 0: - raise ValueError("int_input must be greater than 0") - return True - - RETURN_TYPES = () - FUNCTION = "execute" - CATEGORY = "DevTools" - DESCRIPTION = "A node that validates an input" - OUTPUT_NODE = True - - def execute(self, int_input: int): - print(f"int_input: {int_input}") - return tuple() - -class NodeWithV2ComboInput: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "combo_input": ( - "COMBO", - {"options": ["A", "B"]}, - ), - } - } - - RETURN_TYPES = ("COMBO",) - FUNCTION = "node_with_v2_combo_input" - CATEGORY = "DevTools" - DESCRIPTION = ( - "A node that outputs a combo type that adheres to the v2 combo input spec" - ) - - def node_with_v2_combo_input(self, combo_input: str): - return (combo_input,) - - -NODE_CLASS_MAPPINGS = { - "DevToolsErrorRaiseNode": ErrorRaiseNode, - "DevToolsErrorRaiseNodeWithMessage": ErrorRaiseNodeWithMessage, - "DevToolsExperimentalNode": ExperimentalNode, - "DevToolsDeprecatedNode": DeprecatedNode, - "DevToolsLongComboDropdown": LongComboDropdown, - "DevToolsNodeWithOptionalInput": NodeWithOptionalInput, - "DevToolsNodeWithOptionalComboInput": NodeWithOptionalComboInput, - "DevToolsNodeWithOnlyOptionalInput": NodeWithOnlyOptionalInput, - "DevToolsNodeWithOutputList": NodeWithOutputList, - "DevToolsNodeWithForceInput": NodeWithForceInput, - "DevToolsNodeWithDefaultInput": NodeWithDefaultInput, - "DevToolsNodeWithStringInput": NodeWithStringInput, - "DevToolsNodeWithUnionInput": NodeWithUnionInput, - "DevToolsSimpleSlider": SimpleSlider, - "DevToolsNodeWithSeedInput": NodeWithSeedInput, - "DevToolsObjectPatchNode": ObjectPatchNode, - "DevToolsNodeWithBooleanInput": NodeWithBooleanInput, - "DevToolsRemoteWidgetNode": RemoteWidgetNode, - "DevToolsRemoteWidgetNodeWithParams": RemoteWidgetNodeWithParams, - "DevToolsRemoteWidgetNodeWithRefresh": RemoteWidgetNodeWithRefresh, - "DevToolsRemoteWidgetNodeWithRefreshButton": RemoteWidgetNodeWithRefreshButton, - "DevToolsRemoteWidgetNodeWithControlAfterRefresh": RemoteWidgetNodeWithControlAfterRefresh, - "DevToolsNodeWithOutputCombo": NodeWithOutputCombo, - "DevToolsMultiSelectNode": MultiSelectNode, - "DevToolsLoadAnimatedImageTest": LoadAnimatedImageTest, - "DevToolsNodeWithValidation": NodeWithValidation, - "DevToolsNodeWithV2ComboInput": NodeWithV2ComboInput, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "DevToolsErrorRaiseNode": "Raise Error", - "DevToolsErrorRaiseNodeWithMessage": "Raise Error with Message", - "DevToolsExperimentalNode": "Experimental Node", - "DevToolsDeprecatedNode": "Deprecated Node", - "DevToolsLongComboDropdown": "Long Combo Dropdown", - "DevToolsNodeWithOptionalInput": "Node With Optional Input", - "DevToolsNodeWithOptionalComboInput": "Node With Optional Combo Input", - "DevToolsNodeWithOnlyOptionalInput": "Node With Only Optional Input", - "DevToolsNodeWithOutputList": "Node With Output List", - "DevToolsNodeWithForceInput": "Node With Force Input", - "DevToolsNodeWithDefaultInput": "Node With Default Input", - "DevToolsNodeWithStringInput": "Node With String Input", - "DevToolsNodeWithUnionInput": "Node With Union Input", - "DevToolsSimpleSlider": "Simple Slider", - "DevToolsNodeWithSeedInput": "Node With Seed Input", - "DevToolsObjectPatchNode": "Object Patch Node", - "DevToolsNodeWithBooleanInput": "Node With Boolean Input", - "DevToolsRemoteWidgetNode": "Remote Widget Node", - "DevToolsRemoteWidgetNodeWithParams": "Remote Widget Node With Sort Query Param", - "DevToolsRemoteWidgetNodeWithRefresh": "Remote Widget Node With 300ms Refresh", - "DevToolsRemoteWidgetNodeWithRefreshButton": "Remote Widget Node With Refresh Button", - "DevToolsRemoteWidgetNodeWithControlAfterRefresh": "Remote Widget Node With Refresh Button and Control After Refresh", - "DevToolsNodeWithOutputCombo": "Node With Output Combo", - "DevToolsMultiSelectNode": "Multi Select Node", - "DevToolsLoadAnimatedImageTest": "Load Animated Image", - "DevToolsNodeWithValidation": "Node With Validation", - "DevToolsNodeWithV2ComboInput": "Node With V2 Combo Input", -} \ No newline at end of file +from __future__ import annotations + +from .nodes import ( + DeprecatedNode, + DummyPatch, + ErrorRaiseNode, + ErrorRaiseNodeWithMessage, + ExperimentalNode, + LoadAnimatedImageTest, + LongComboDropdown, + MultiSelectNode, + NodeWithBooleanInput, + NodeWithDefaultInput, + NodeWithForceInput, + NodeWithOptionalComboInput, + NodeWithOptionalInput, + NodeWithOnlyOptionalInput, + NodeWithOutputCombo, + NodeWithOutputList, + NodeWithSeedInput, + NodeWithStringInput, + NodeWithUnionInput, + NodeWithValidation, + NodeWithV2ComboInput, + ObjectPatchNode, + RemoteWidgetNode, + RemoteWidgetNodeWithControlAfterRefresh, + RemoteWidgetNodeWithParams, + RemoteWidgetNodeWithRefresh, + RemoteWidgetNodeWithRefreshButton, + SimpleSlider, + NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS, +) + +__all__ = [ + "DeprecatedNode", + "DummyPatch", + "ErrorRaiseNode", + "ErrorRaiseNodeWithMessage", + "ExperimentalNode", + "LoadAnimatedImageTest", + "LongComboDropdown", + "MultiSelectNode", + "NodeWithBooleanInput", + "NodeWithDefaultInput", + "NodeWithForceInput", + "NodeWithOptionalComboInput", + "NodeWithOptionalInput", + "NodeWithOnlyOptionalInput", + "NodeWithOutputCombo", + "NodeWithOutputList", + "NodeWithSeedInput", + "NodeWithStringInput", + "NodeWithUnionInput", + "NodeWithValidation", + "NodeWithV2ComboInput", + "ObjectPatchNode", + "RemoteWidgetNode", + "RemoteWidgetNodeWithControlAfterRefresh", + "RemoteWidgetNodeWithParams", + "RemoteWidgetNodeWithRefresh", + "RemoteWidgetNodeWithRefreshButton", + "SimpleSlider", + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", +] diff --git a/tools/devtools/nodes/__init__.py b/tools/devtools/nodes/__init__.py new file mode 100644 index 000000000..f0ac2d8ee --- /dev/null +++ b/tools/devtools/nodes/__init__.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from .errors import ( + DeprecatedNode, + ErrorRaiseNode, + ErrorRaiseNodeWithMessage, + ExperimentalNode, + NODE_CLASS_MAPPINGS as errors_class_mappings, + NODE_DISPLAY_NAME_MAPPINGS as errors_display_name_mappings, +) +from .inputs import ( + LongComboDropdown, + NodeWithBooleanInput, + NodeWithDefaultInput, + NodeWithForceInput, + NodeWithOptionalComboInput, + NodeWithOptionalInput, + NodeWithOnlyOptionalInput, + NodeWithOutputList, + NodeWithSeedInput, + NodeWithStringInput, + NodeWithUnionInput, + NodeWithValidation, + NodeWithV2ComboInput, + SimpleSlider, + NODE_CLASS_MAPPINGS as inputs_class_mappings, + NODE_DISPLAY_NAME_MAPPINGS as inputs_display_name_mappings, +) +from .models import ( + DummyPatch, + LoadAnimatedImageTest, + ObjectPatchNode, + NODE_CLASS_MAPPINGS as models_class_mappings, + NODE_DISPLAY_NAME_MAPPINGS as models_display_name_mappings, +) +from .remote import ( + MultiSelectNode, + NodeWithOutputCombo, + RemoteWidgetNode, + RemoteWidgetNodeWithControlAfterRefresh, + RemoteWidgetNodeWithParams, + RemoteWidgetNodeWithRefresh, + RemoteWidgetNodeWithRefreshButton, + NODE_CLASS_MAPPINGS as remote_class_mappings, + NODE_DISPLAY_NAME_MAPPINGS as remote_display_name_mappings, +) + +NODE_CLASS_MAPPINGS = { + **errors_class_mappings, + **inputs_class_mappings, + **remote_class_mappings, + **models_class_mappings, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + **errors_display_name_mappings, + **inputs_display_name_mappings, + **remote_display_name_mappings, + **models_display_name_mappings, +} + +__all__ = [ + "DeprecatedNode", + "DummyPatch", + "ErrorRaiseNode", + "ErrorRaiseNodeWithMessage", + "ExperimentalNode", + "LoadAnimatedImageTest", + "LongComboDropdown", + "MultiSelectNode", + "NodeWithBooleanInput", + "NodeWithDefaultInput", + "NodeWithForceInput", + "NodeWithOptionalComboInput", + "NodeWithOptionalInput", + "NodeWithOnlyOptionalInput", + "NodeWithOutputCombo", + "NodeWithOutputList", + "NodeWithSeedInput", + "NodeWithStringInput", + "NodeWithUnionInput", + "NodeWithValidation", + "NodeWithV2ComboInput", + "ObjectPatchNode", + "RemoteWidgetNode", + "RemoteWidgetNodeWithControlAfterRefresh", + "RemoteWidgetNodeWithParams", + "RemoteWidgetNodeWithRefresh", + "RemoteWidgetNodeWithRefreshButton", + "SimpleSlider", + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", +] diff --git a/tools/devtools/nodes/errors.py b/tools/devtools/nodes/errors.py new file mode 100644 index 000000000..0725208fb --- /dev/null +++ b/tools/devtools/nodes/errors.py @@ -0,0 +1,89 @@ +from __future__ import annotations + + +class ErrorRaiseNode: + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "raise_error" + CATEGORY = "DevTools" + DESCRIPTION = "Raise an error for development purposes" + + def raise_error(self): + raise Exception("Error node was called!") + + +class ErrorRaiseNodeWithMessage: + @classmethod + def INPUT_TYPES(cls): + return {"required": {"message": ("STRING", {"multiline": True})}} + + RETURN_TYPES = () + OUTPUT_NODE = True + + FUNCTION = "raise_error" + CATEGORY = "DevTools" + DESCRIPTION = "Raise an error with message for development purposes" + + def raise_error(self, message: str): + raise Exception(message) + + +class ExperimentalNode: + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "experimental_function" + CATEGORY = "DevTools" + DESCRIPTION = "A experimental node" + + EXPERIMENTAL = True + + def experimental_function(self): + print("Experimental node was called!") + + +class DeprecatedNode: + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "deprecated_function" + CATEGORY = "DevTools" + DESCRIPTION = "A deprecated node" + + DEPRECATED = True + + def deprecated_function(self): + print("Deprecated node was called!") + + +NODE_CLASS_MAPPINGS = { + "DevToolsErrorRaiseNode": ErrorRaiseNode, + "DevToolsErrorRaiseNodeWithMessage": ErrorRaiseNodeWithMessage, + "DevToolsExperimentalNode": ExperimentalNode, + "DevToolsDeprecatedNode": DeprecatedNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DevToolsErrorRaiseNode": "Raise Error", + "DevToolsErrorRaiseNodeWithMessage": "Raise Error with Message", + "DevToolsExperimentalNode": "Experimental Node", + "DevToolsDeprecatedNode": "Deprecated Node", +} + +__all__ = [ + "ErrorRaiseNode", + "ErrorRaiseNodeWithMessage", + "ExperimentalNode", + "DeprecatedNode", + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", +] diff --git a/tools/devtools/nodes/inputs.py b/tools/devtools/nodes/inputs.py new file mode 100644 index 000000000..ac31056ca --- /dev/null +++ b/tools/devtools/nodes/inputs.py @@ -0,0 +1,357 @@ +from __future__ import annotations + +import time + + +class LongComboDropdown: + @classmethod + def INPUT_TYPES(cls): + return {"required": {"option": ([f"Option {i}" for i in range(1_000)],)}} + + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "long_combo_dropdown" + CATEGORY = "DevTools" + DESCRIPTION = "A long combo dropdown" + + def long_combo_dropdown(self, option: str): + print(option) + + +class NodeWithOptionalInput: + @classmethod + def INPUT_TYPES(cls): + return { + "required": {"required_input": ("IMAGE",)}, + "optional": {"optional_input": ("IMAGE", {"default": None})}, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "node_with_optional_input" + CATEGORY = "DevTools" + DESCRIPTION = "A node with an optional input" + + def node_with_optional_input(self, required_input, optional_input=None): + print( + f"Calling node with required_input: {required_input} and optional_input: {optional_input}" + ) + return (required_input,) + + +class NodeWithOptionalComboInput: + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "optional_combo_input": ( + [f"Random Unique Option {time.time()}" for _ in range(8)], + {"default": None}, + ) + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "node_with_optional_combo_input" + CATEGORY = "DevTools" + DESCRIPTION = "A node with an optional combo input that returns unique values every time INPUT_TYPES is called" + + def node_with_optional_combo_input(self, optional_combo_input=None): + print(f"Calling node with optional_combo_input: {optional_combo_input}") + return (optional_combo_input,) + + +class NodeWithOnlyOptionalInput: + @classmethod + def INPUT_TYPES(s): + return { + "optional": { + "text": ("STRING", {"multiline": True, "dynamicPrompts": True}), + "clip": ("CLIP", {}), + } + } + + RETURN_TYPES = () + FUNCTION = "node_with_only_optional_input" + CATEGORY = "DevTools" + DESCRIPTION = "A node with only optional input" + + def node_with_only_optional_input(self, clip=None, text=None): + pass + + +class NodeWithOutputList: + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = ( + "INT", + "INT", + ) + RETURN_NAMES = ( + "INTEGER OUTPUT", + "INTEGER LIST OUTPUT", + ) + OUTPUT_IS_LIST = ( + False, + True, + ) + FUNCTION = "node_with_output_list" + CATEGORY = "DevTools" + DESCRIPTION = "A node with an output list" + + def node_with_output_list(self): + return (1, [1, 2, 3]) + + +class NodeWithForceInput: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "int_input": ("INT", {"forceInput": True}), + "int_input_widget": ("INT", {"default": 1}), + }, + "optional": {"float_input": ("FLOAT", {"forceInput": True})}, + } + + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "node_with_force_input" + CATEGORY = "DevTools" + DESCRIPTION = "A node with a forced input" + + def node_with_force_input( + self, int_input: int, int_input_widget: int, float_input: float = 0.0 + ): + print( + f"int_input: {int_input}, int_input_widget: {int_input_widget}, float_input: {float_input}" + ) + + +class NodeWithDefaultInput: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "int_input": ("INT", {"defaultInput": True}), + "int_input_widget": ("INT", {"default": 1}), + }, + "optional": {"float_input": ("FLOAT", {"defaultInput": True})}, + } + + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "node_with_default_input" + CATEGORY = "DevTools" + DESCRIPTION = "A node with a default input" + + def node_with_default_input( + self, int_input: int, int_input_widget: int, float_input: float = 0.0 + ): + print( + f"int_input: {int_input}, int_input_widget: {int_input_widget}, float_input: {float_input}" + ) + + +class NodeWithStringInput: + @classmethod + def INPUT_TYPES(cls): + return {"required": {"string_input": ("STRING",)}} + + RETURN_TYPES = () + FUNCTION = "node_with_string_input" + CATEGORY = "DevTools" + DESCRIPTION = "A node with a string input" + + def node_with_string_input(self, string_input: str): + print(f"string_input: {string_input}") + + +class NodeWithUnionInput: + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "string_or_int_input": ("STRING,INT",), + "string_input": ("STRING", {"forceInput": True}), + "int_input": ("INT", {"forceInput": True}), + } + } + + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "node_with_union_input" + CATEGORY = "DevTools" + DESCRIPTION = "A node with a union input" + + def node_with_union_input( + self, + string_or_int_input: str | int = "", + string_input: str = "", + int_input: int = 0, + ): + print( + f"string_or_int_input: {string_or_int_input}, string_input: {string_input}, int_input: {int_input}" + ) + return { + "ui": { + "text": string_or_int_input, + } + } + + +class NodeWithBooleanInput: + @classmethod + def INPUT_TYPES(cls): + return {"required": {"boolean_input": ("BOOLEAN",)}} + + RETURN_TYPES = () + FUNCTION = "node_with_boolean_input" + CATEGORY = "DevTools" + DESCRIPTION = "A node with a boolean input" + + def node_with_boolean_input(self, boolean_input: bool): + print(f"boolean_input: {boolean_input}") + + +class SimpleSlider: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ( + "FLOAT", + { + "display": "slider", + "default": 0.5, + "min": 0.0, + "max": 1.0, + "step": 0.001, + }, + ), + }, + } + + RETURN_TYPES = ("FLOAT",) + FUNCTION = "execute" + CATEGORY = "DevTools" + + def execute(self, value): + return (value,) + + +class NodeWithSeedInput: + @classmethod + def INPUT_TYPES(cls): + return {"required": {"seed": ("INT", {"default": 0})}} + + RETURN_TYPES = () + FUNCTION = "node_with_seed_input" + CATEGORY = "DevTools" + DESCRIPTION = "A node with a seed input" + OUTPUT_NODE = True + + def node_with_seed_input(self, seed: int): + print(f"seed: {seed}") + + +class NodeWithValidation: + @classmethod + def INPUT_TYPES(cls): + return { + "required": {"int_input": ("INT",)}, + } + + @classmethod + def VALIDATE_INPUTS(cls, int_input: int): + if int_input < 0: + raise ValueError("int_input must be greater than 0") + return True + + RETURN_TYPES = () + FUNCTION = "execute" + CATEGORY = "DevTools" + DESCRIPTION = "A node that validates an input" + OUTPUT_NODE = True + + def execute(self, int_input: int): + print(f"int_input: {int_input}") + return tuple() + + +class NodeWithV2ComboInput: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "combo_input": ( + "COMBO", + {"options": ["A", "B"]}, + ), + } + } + + RETURN_TYPES = ("COMBO",) + FUNCTION = "node_with_v2_combo_input" + CATEGORY = "DevTools" + DESCRIPTION = ( + "A node that outputs a combo type that adheres to the v2 combo input spec" + ) + + def node_with_v2_combo_input(self, combo_input: str): + return (combo_input,) + + +NODE_CLASS_MAPPINGS = { + "DevToolsLongComboDropdown": LongComboDropdown, + "DevToolsNodeWithOptionalInput": NodeWithOptionalInput, + "DevToolsNodeWithOptionalComboInput": NodeWithOptionalComboInput, + "DevToolsNodeWithOnlyOptionalInput": NodeWithOnlyOptionalInput, + "DevToolsNodeWithOutputList": NodeWithOutputList, + "DevToolsNodeWithForceInput": NodeWithForceInput, + "DevToolsNodeWithDefaultInput": NodeWithDefaultInput, + "DevToolsNodeWithStringInput": NodeWithStringInput, + "DevToolsNodeWithUnionInput": NodeWithUnionInput, + "DevToolsNodeWithBooleanInput": NodeWithBooleanInput, + "DevToolsSimpleSlider": SimpleSlider, + "DevToolsNodeWithSeedInput": NodeWithSeedInput, + "DevToolsNodeWithValidation": NodeWithValidation, + "DevToolsNodeWithV2ComboInput": NodeWithV2ComboInput, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DevToolsLongComboDropdown": "Long Combo Dropdown", + "DevToolsNodeWithOptionalInput": "Node With Optional Input", + "DevToolsNodeWithOptionalComboInput": "Node With Optional Combo Input", + "DevToolsNodeWithOnlyOptionalInput": "Node With Only Optional Input", + "DevToolsNodeWithOutputList": "Node With Output List", + "DevToolsNodeWithForceInput": "Node With Force Input", + "DevToolsNodeWithDefaultInput": "Node With Default Input", + "DevToolsNodeWithStringInput": "Node With String Input", + "DevToolsNodeWithUnionInput": "Node With Union Input", + "DevToolsNodeWithBooleanInput": "Node With Boolean Input", + "DevToolsSimpleSlider": "Simple Slider", + "DevToolsNodeWithSeedInput": "Node With Seed Input", + "DevToolsNodeWithValidation": "Node With Validation", + "DevToolsNodeWithV2ComboInput": "Node With V2 Combo Input", +} + +__all__ = [ + "LongComboDropdown", + "NodeWithOptionalInput", + "NodeWithOptionalComboInput", + "NodeWithOnlyOptionalInput", + "NodeWithOutputList", + "NodeWithForceInput", + "NodeWithDefaultInput", + "NodeWithStringInput", + "NodeWithUnionInput", + "NodeWithBooleanInput", + "SimpleSlider", + "NodeWithSeedInput", + "NodeWithValidation", + "NodeWithV2ComboInput", + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", +] diff --git a/tools/devtools/nodes/models.py b/tools/devtools/nodes/models.py new file mode 100644 index 000000000..f40b1b0f5 --- /dev/null +++ b/tools/devtools/nodes/models.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import os + +import torch + +import comfy.utils as utils +from comfy.model_patcher import ModelPatcher +import nodes +import folder_paths + + +class DummyPatch(torch.nn.Module): + def __init__(self, module: torch.nn.Module, dummy_float: float = 0.0): + super().__init__() + self.module = module + self.dummy_float = dummy_float + + def forward(self, *args, **kwargs): + if isinstance(self.module, DummyPatch): + raise Exception(f"Calling nested dummy patch! {self.dummy_float}") + + return self.module(*args, **kwargs) + + +class ObjectPatchNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "target_module": ("STRING", {"multiline": True}), + }, + "optional": { + "dummy_float": ("FLOAT", {"default": 0.0}), + }, + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "apply_patch" + CATEGORY = "DevTools" + DESCRIPTION = "A node that applies an object patch" + + def apply_patch( + self, model: ModelPatcher, target_module: str, dummy_float: float = 0.0 + ) -> ModelPatcher: + module = utils.get_attr(model.model, target_module) + work_model = model.clone() + work_model.add_object_patch(target_module, DummyPatch(module, dummy_float)) + return (work_model,) + + +class LoadAnimatedImageTest(nodes.LoadImage): + @classmethod + def INPUT_TYPES(s): + input_dir = folder_paths.get_input_directory() + files = [ + f + for f in os.listdir(input_dir) + if os.path.isfile(os.path.join(input_dir, f)) and f.endswith(".webp") + ] + files = folder_paths.filter_files_content_types(files, ["image"]) + return { + "required": {"image": (sorted(files), {"animated_image_upload": True})}, + } + + +NODE_CLASS_MAPPINGS = { + "DevToolsObjectPatchNode": ObjectPatchNode, + "DevToolsLoadAnimatedImageTest": LoadAnimatedImageTest, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DevToolsObjectPatchNode": "Object Patch Node", + "DevToolsLoadAnimatedImageTest": "Load Animated Image", +} + +__all__ = [ + "DummyPatch", + "ObjectPatchNode", + "LoadAnimatedImageTest", + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", +] diff --git a/tools/devtools/nodes/remote.py b/tools/devtools/nodes/remote.py new file mode 100644 index 000000000..12561ec8d --- /dev/null +++ b/tools/devtools/nodes/remote.py @@ -0,0 +1,220 @@ +from __future__ import annotations + + +class RemoteWidgetNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "remote_widget_value": ( + "COMBO", + { + "remote": { + "route": "/api/models/checkpoints", + }, + }, + ), + }, + } + + FUNCTION = "remote_widget" + CATEGORY = "DevTools" + DESCRIPTION = "A node that lazily fetches options from a remote endpoint" + RETURN_TYPES = ("STRING",) + + def remote_widget(self, remote_widget_value: str): + return (remote_widget_value,) + + +class RemoteWidgetNodeWithParams: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "remote_widget_value": ( + "COMBO", + { + "remote": { + "route": "/api/models/checkpoints", + "query_params": { + "sort": "true", + }, + }, + }, + ), + }, + } + + FUNCTION = "remote_widget" + CATEGORY = "DevTools" + DESCRIPTION = ( + "A node that lazily fetches options from a remote endpoint with query params" + ) + RETURN_TYPES = ("STRING",) + + def remote_widget(self, remote_widget_value: str): + return (remote_widget_value,) + + +class RemoteWidgetNodeWithRefresh: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "remote_widget_value": ( + "COMBO", + { + "remote": { + "route": "/api/models/checkpoints", + "refresh": 300, + "max_retries": 10, + "timeout": 256, + }, + }, + ), + }, + } + + FUNCTION = "remote_widget" + CATEGORY = "DevTools" + DESCRIPTION = "A node that lazily fetches options from a remote endpoint and refresh the options every 300 ms" + RETURN_TYPES = ("STRING",) + + def remote_widget(self, remote_widget_value: str): + return (remote_widget_value,) + + +class RemoteWidgetNodeWithRefreshButton: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "remote_widget_value": ( + "COMBO", + { + "remote": { + "route": "/api/models/checkpoints", + "refresh_button": True, + }, + }, + ), + }, + } + + FUNCTION = "remote_widget" + CATEGORY = "DevTools" + DESCRIPTION = "A node that lazily fetches options from a remote endpoint and has a refresh button to manually reload options" + RETURN_TYPES = ("STRING",) + + def remote_widget(self, remote_widget_value: str): + return (remote_widget_value,) + + +class RemoteWidgetNodeWithControlAfterRefresh: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "remote_widget_value": ( + "COMBO", + { + "remote": { + "route": "/api/models/checkpoints", + "refresh_button": True, + "control_after_refresh": "first", + }, + }, + ), + }, + } + + FUNCTION = "remote_widget" + CATEGORY = "DevTools" + DESCRIPTION = "A node that lazily fetches options from a remote endpoint and has a refresh button to manually reload options and select the first option on refresh" + RETURN_TYPES = ("STRING",) + + def remote_widget(self, remote_widget_value: str): + return (remote_widget_value,) + + +class NodeWithOutputCombo: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "subset_options": (["A", "B"], {"forceInput": True}), + "subset_options_v2": ( + "COMBO", + {"options": ["A", "B"], "forceInput": True}, + ), + } + } + + RETURN_TYPES = (["A", "B", "C"],) + FUNCTION = "node_with_output_combo" + CATEGORY = "DevTools" + DESCRIPTION = "A node that outputs a combo type" + + def node_with_output_combo(self, subset_options: str): + return (subset_options,) + + +class MultiSelectNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "foo": ( + "COMBO", + { + "options": ["A", "B", "C"], + "multi_select": { + "placeholder": "Choose foos", + "chip": True, + }, + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + OUTPUT_IS_LIST = [True] + FUNCTION = "multi_select_node" + CATEGORY = "DevTools" + DESCRIPTION = "A node that outputs a multi select type" + + def multi_select_node(self, foo: list[str]) -> list[str]: + return (foo,) + + +NODE_CLASS_MAPPINGS = { + "DevToolsRemoteWidgetNode": RemoteWidgetNode, + "DevToolsRemoteWidgetNodeWithParams": RemoteWidgetNodeWithParams, + "DevToolsRemoteWidgetNodeWithRefresh": RemoteWidgetNodeWithRefresh, + "DevToolsRemoteWidgetNodeWithRefreshButton": RemoteWidgetNodeWithRefreshButton, + "DevToolsRemoteWidgetNodeWithControlAfterRefresh": RemoteWidgetNodeWithControlAfterRefresh, + "DevToolsNodeWithOutputCombo": NodeWithOutputCombo, + "DevToolsMultiSelectNode": MultiSelectNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DevToolsRemoteWidgetNode": "Remote Widget Node", + "DevToolsRemoteWidgetNodeWithParams": "Remote Widget Node With Sort Query Param", + "DevToolsRemoteWidgetNodeWithRefresh": "Remote Widget Node With 300ms Refresh", + "DevToolsRemoteWidgetNodeWithRefreshButton": "Remote Widget Node With Refresh Button", + "DevToolsRemoteWidgetNodeWithControlAfterRefresh": "Remote Widget Node With Refresh Button and Control After Refresh", + "DevToolsNodeWithOutputCombo": "Node With Output Combo", + "DevToolsMultiSelectNode": "Multi Select Node", +} + +__all__ = [ + "RemoteWidgetNode", + "RemoteWidgetNodeWithParams", + "RemoteWidgetNodeWithRefresh", + "RemoteWidgetNodeWithRefreshButton", + "RemoteWidgetNodeWithControlAfterRefresh", + "NodeWithOutputCombo", + "MultiSelectNode", + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", +]