mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
[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:
28
tools/devtools/README.md
Normal file
28
tools/devtools/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# ComfyUI DevTools
|
||||
|
||||
This directory contains development tools and test utilities for ComfyUI, previously maintained as a separate repository at `https://github.com/Comfy-Org/ComfyUI_devtools`.
|
||||
|
||||
## Contents
|
||||
|
||||
- `__init__.py` - Server endpoints for development tools (`/api/devtools/*`)
|
||||
- `dev_nodes.py` - Development and testing nodes for ComfyUI
|
||||
- `fake_model.safetensors` - Test fixture for model loading tests
|
||||
|
||||
## Purpose
|
||||
|
||||
These tools provide:
|
||||
- Test endpoints for browser automation
|
||||
- Development nodes for testing various UI features
|
||||
- Mock data for consistent testing environments
|
||||
|
||||
## Usage
|
||||
|
||||
During CI/CD, these files are automatically copied to the ComfyUI `custom_nodes` directory. For local development, copy these files to your ComfyUI installation:
|
||||
|
||||
```bash
|
||||
cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
This directory was created as part of issue #4683 to merge the ComfyUI_devtools repository into the main frontend repository, eliminating the need for separate versioning and simplifying the development workflow.
|
||||
100
tools/devtools/__init__.py
Normal file
100
tools/devtools/__init__.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
from .dev_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
from typing import Union
|
||||
|
||||
import server
|
||||
from aiohttp import web
|
||||
from aiohttp.web_request import Request
|
||||
import folder_paths
|
||||
from folder_paths import models_dir
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/devtools/fake_model.safetensors")
|
||||
async def fake_model(request: Request):
|
||||
file_path = os.path.join(os.path.dirname(__file__), "fake_model.safetensors")
|
||||
return web.FileResponse(file_path)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/devtools/cleanup_fake_model")
|
||||
async def cleanup_fake_model(request: Request):
|
||||
model_folder = request.query.get("model_folder", "clip")
|
||||
model_path = os.path.join(models_dir, model_folder, "fake_model.safetensors")
|
||||
if os.path.exists(model_path):
|
||||
os.remove(model_path)
|
||||
return web.Response(status=200, text="Fake model cleaned up")
|
||||
|
||||
|
||||
TreeType = dict[str, Union[str, "TreeType"]]
|
||||
|
||||
|
||||
def write_tree_structure(tree: TreeType, base_path: str):
|
||||
# Remove existing files and folders in users/workflows
|
||||
if os.path.exists(base_path):
|
||||
shutil.rmtree(base_path)
|
||||
|
||||
# Recreate the base directory
|
||||
os.makedirs(base_path, exist_ok=True)
|
||||
|
||||
def write_recursive(current_tree: TreeType, current_path: str):
|
||||
for key, value in current_tree.items():
|
||||
new_path = os.path.join(current_path, key)
|
||||
if isinstance(value, dict):
|
||||
# If it's a dictionary, create a new directory and recurse
|
||||
os.makedirs(new_path, exist_ok=True)
|
||||
write_recursive(value, new_path)
|
||||
else:
|
||||
# If it's a string, write the content to a file
|
||||
with open(new_path, "w") as f:
|
||||
f.write(value)
|
||||
|
||||
write_recursive(tree, base_path)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/devtools/setup_folder_structure")
|
||||
async def setup_folder_structure(request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
tree_structure = data.get("tree_structure")
|
||||
base_path = os.path.join(
|
||||
folder_paths.base_path, data.get("base_path", "users/workflows")
|
||||
)
|
||||
|
||||
if not isinstance(tree_structure, dict):
|
||||
return web.Response(status=400, text="Invalid tree structure")
|
||||
|
||||
write_tree_structure(tree_structure, base_path)
|
||||
return web.Response(status=200, text=f"Folder structure created at {base_path}")
|
||||
except json.JSONDecodeError:
|
||||
return web.Response(status=400, text="Invalid JSON data")
|
||||
except Exception as e:
|
||||
return web.Response(status=500, text=f"Error: {str(e)}")
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/devtools/set_settings")
|
||||
async def set_settings(request: Request):
|
||||
"""Directly set the settings for the user specified via `Comfy.userId`,
|
||||
instead of merging with the existing settings."""
|
||||
try:
|
||||
settings: dict[str, str | bool | int | float] = await request.json()
|
||||
user_root = folder_paths.get_user_directory()
|
||||
try:
|
||||
user_id: str = settings.pop("Comfy.userId")
|
||||
except KeyError:
|
||||
user_id = "default"
|
||||
settings_file_path = os.path.join(user_root, user_id, "comfy.settings.json")
|
||||
|
||||
# Ensure the directory structure exists
|
||||
os.makedirs(os.path.dirname(settings_file_path), exist_ok=True)
|
||||
|
||||
with open(settings_file_path, "w") as f:
|
||||
f.write(json.dumps(settings, indent=4))
|
||||
return web.Response(status=200)
|
||||
except Exception as e:
|
||||
return web.Response(status=500, text=f"Error: {str(e)}")
|
||||
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
673
tools/devtools/dev_nodes.py
Normal file
673
tools/devtools/dev_nodes.py
Normal 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",
|
||||
}
|
||||
1
tools/devtools/fake_model.safetensors
Normal file
1
tools/devtools/fake_model.safetensors
Normal file
@@ -0,0 +1 @@
|
||||
This is a fake model file used for testing.
|
||||
Reference in New Issue
Block a user