From 99a5f71b2c79662e09c7639d0dfc2380c6aa2990 Mon Sep 17 00:00:00 2001 From: bennykok Date: Wed, 16 Apr 2025 11:52:20 +0800 Subject: [PATCH] feat: add external enum node --- __init__.py | 38 +++++++++++++++- comfy-nodes/external_enum.py | 46 +++++++++++++++++++ web-plugin/index.js | 87 +++++++++++++++++++++++++++++++++--- 3 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 comfy-nodes/external_enum.py diff --git a/__init__.py b/__init__.py index 539bcb5..a3854d4 100644 --- a/__init__.py +++ b/__init__.py @@ -2,8 +2,9 @@ @author: BennyKok @title: comfyui-deploy @nickname: Comfy Deploy -@description: +@description: """ + import os import sys @@ -17,19 +18,23 @@ import requests import folder_paths from folder_paths import add_model_folder_path, get_filename_list, get_folder_paths from tqdm import tqdm +import re from . import custom_routes # import routes ag_path = os.path.join(os.path.dirname(__file__)) + def get_python_files(path): return [f[:-3] for f in os.listdir(path) if f.endswith(".py")] + def append_to_sys_path(path): if path not in sys.path: sys.path.append(path) + paths = ["comfy-nodes"] files = [] @@ -41,14 +46,45 @@ for path in paths: NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} + +def split_camel_case(name): + # Split on underscores first, then split each part on camelCase + parts = [] + for part in name.split("_"): + # Find all camelCase boundaries + words = re.findall("[A-Z][^A-Z]*", part) + if not words: # If no camelCase found, use the whole part + words = [part] + parts.extend(words) + return parts + + # Import all the modules and append their mappings for file in files: module = importlib.import_module(file) + # Check if the module has explicit mappings if hasattr(module, "NODE_CLASS_MAPPINGS"): NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS) if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS"): NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS) + # Auto-discover classes with ComfyUI node attributes + for name, obj in inspect.getmembers(module): + # Check if it's a class and has the required ComfyUI node attributes + if ( + inspect.isclass(obj) + and hasattr(obj, "INPUT_TYPES") + and hasattr(obj, "RETURN_TYPES") + ): + # Use the class name as the key if not already in mappings + if name not in NODE_CLASS_MAPPINGS: + NODE_CLASS_MAPPINGS[name] = obj + # Create a display name by converting camelCase to Title Case with spaces + words = split_camel_case(name.replace("ComfyUIDeploy", "")) + display_name = " ".join(word.capitalize() for word in words) + # print(display_name, name) + NODE_DISPLAY_NAME_MAPPINGS[name] = display_name + WEB_DIRECTORY = "web-plugin" __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/comfy-nodes/external_enum.py b/comfy-nodes/external_enum.py new file mode 100644 index 0000000..2402c2a --- /dev/null +++ b/comfy-nodes/external_enum.py @@ -0,0 +1,46 @@ +class AnyType(str): + def __ne__(self, __value: object) -> bool: + return False + + +WILDCARD = AnyType("*") + +class ComfyUIDeployExternalEnum: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input_id": ( + "STRING", + {"multiline": False, "default": "input_enum"}, + ), + }, + "optional": { + "default_value": ( + "STRING", + {"multiline": False, "default": "", "dynamic_enum": True}, + ), + "options": ( + "STRING", + {"multiline": True, "default": ""}, + ), + "display_name": ( + "STRING", + {"multiline": False, "default": ""}, + ), + "description": ( + "STRING", + {"multiline": True, "default": ""}, + ), + } + } + + RETURN_TYPES = (WILDCARD,) + RETURN_NAMES = ("text",) + + FUNCTION = "run" + + CATEGORY = "🔗ComfyDeploy" + + def run(self, input_id, default_value=None, display_name=None, description=None): + return [default_value] \ No newline at end of file diff --git a/web-plugin/index.js b/web-plugin/index.js index 8b99e9c..6b86281 100644 --- a/web-plugin/index.js +++ b/web-plugin/index.js @@ -173,6 +173,7 @@ const VALID_TYPES = [ "BOOLEAN", "text", "string", + "combo", ]; function hideWidget(node, widget, suffix = "") { @@ -214,6 +215,8 @@ function getWidgetType(config) { async function convertToInput(node, widget, config) { const { type } = getWidgetType(config); + console.log(config); + const result = await app.extensionManager.dialog.prompt( { title: "Convert " + widget.name + " to external input", @@ -291,24 +294,56 @@ async function convertToInput(node, widget, config) { externalNode = "ComfyUIDeployExternalNumberSlider"; // inputId = "input_number"; } - + if (type === "STRING") { externalNode = "ComfyUIDeployExternalText"; // inputId = "input_text"; } + if (type === "COMBO") { + externalNode = "ComfyUIDeployExternalEnum"; + // inputId = "input_enum"; + } + if (!externalNode || !inputId) return; node.convertWidgetToInput(widget); - var inputNode = LiteGraph.createNode(externalNode); + var inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId); + + // if (type === "COMBO") { + // inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId, { + // dynamic_enum_options: config[0], + // }); + // console.log(inputNode); + // const options = config[0]; + // console.log(options); + // } else { + // inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId); + // } + const index = node.inputs.findIndex((x) => x.name == widget.name); - inputNode.configure({ - widgets_values: [inputId, widget.value], - }); + if (type === "COMBO") { + inputNode.configure({ + widgets_values: [inputId, widget.value, JSON.stringify(config[0])], + }); + } else + { + inputNode.configure({ + widgets_values: [inputId, widget.value], + }); + } inputNode.id = ++app.graph.last_node_id; inputNode.pos = node.pos; inputNode.pos[0] -= node.size[0] + 160; + + if (type === "COMBO") { + console.log(inputNode); + const options = config[0]; + console.log(options); + inputNode.widgets.find((x) => x.name == "default_value").options.values = options; + } + app.graph.add(inputNode); inputNode.connect(0, node, index); @@ -538,6 +573,13 @@ const ext = { console.log(nodeData.input.optional.default_value_url); } + if ( + nodeData?.input?.optional?.default_value?.[1]?.dynamic_enum === true + ) { + nodeData.input.optional.default_value = ["DYNAMIC_ENUM"]; + // console.log(nodeData.input.optional.default_value); + } + // const origonNodeCreated = nodeType.prototype.onNodeCreated; // nodeType.prototype.onNodeCreated = function () { // const r = origonNodeCreated @@ -571,6 +613,10 @@ const ext = { // return r // }; }, + + // async nodeCreated(node) { + + // }, registerCustomNodes() { /** @type {LGraphNode}*/ @@ -733,9 +779,37 @@ const ext = { return { widget: urlWidget }; }, + + DYNAMIC_ENUM(node, inputName, inputData) { + // console.log("DYNAMIC_ENUM", JSON.parse(JSON.stringify(node)), inputName, inputData); + const enumWidget = node.addWidget( + "combo", + inputName, + "", + { serialize: true, values: [] }, + ); + + return { widget: enumWidget }; + }, }; }, + async afterConfigureGraph() { + app.graph.nodes.forEach(node => { + if (node.type === "ComfyUIDeployExternalEnum") { + const default_value_index = node.widgets.findIndex(x => x.name === "default_value"); + const options_index = node.widgets.findIndex(x => x.name === "options"); + + var dynamic_enum_options = [node.widgets[default_value_index].value]; + if (node.widgets[options_index].value) { + dynamic_enum_options = JSON.parse(node.widgets[options_index].value); + } + // console.log("dynamic_enum_options", dynamic_enum_options); + node.widgets[default_value_index].options.values = dynamic_enum_options; + } + }); + }, + async setup() { // const graphCanvas = document.getElementById("graph-canvas"); @@ -923,6 +997,7 @@ const ext = { } })(app.graph.onAfterChange); + sendEventToCD("cd_plugin_setup"); }, }; @@ -1108,6 +1183,8 @@ async function deployWorkflow() { const prompt = await app.graphToPrompt(); let deps = undefined; + console.log(prompt); + if (includeDeps) { loadingDialog.showLoading("Fetching existing version");