[feat] Merge ComfyUI_devtools into ComfyUI_frontend (#5166)

## Summary
Merges ComfyUI devtools components into the ComfyUI frontend monorepo to
consolidate development tools.

## Changes
- Added devtools components from ComfyUI repository
- Integrated development nodes and utilities
- Consolidated fake model assets for testing

## Related Issues
Fixes #4683

## Testing
- Devtools components are now available within the frontend monorepo
- Development workflow remains consistent

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
snomiao
2025-09-23 15:15:50 +09:00
committed by GitHub
parent cec1de0147
commit d0aee031e9
7 changed files with 816 additions and 12 deletions

673
tools/devtools/dev_nodes.py Normal file
View File

@@ -0,0 +1,673 @@
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",
}