From e26869235190d69c551108ec95be6579c5da24f4 Mon Sep 17 00:00:00 2001 From: Sammy Franklin Date: Sat, 7 Oct 2023 00:50:03 -0700 Subject: [PATCH] basic subflow ui functionality --- .gitignore | 1 + comfy_extras/nodes_subflow.py | 29 ++++++++++++++++++ folder_paths.py | 15 ++++++++++ nodes.py | 3 +- server.py | 18 ++++++++++++ web/extensions/core/subflow.js | 54 ++++++++++++++++++++++++++++++++++ web/scripts/api.js | 14 +++++++++ 7 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 comfy_extras/nodes_subflow.py create mode 100644 web/extensions/core/subflow.js diff --git a/.gitignore b/.gitignore index 98d91318d..66284f266 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__/ !/input/example.png /models/ /temp/ +/subflows/ /custom_nodes/ !custom_nodes/example_node.py.example extra_model_paths.yaml diff --git a/comfy_extras/nodes_subflow.py b/comfy_extras/nodes_subflow.py new file mode 100644 index 000000000..c1f6c9833 --- /dev/null +++ b/comfy_extras/nodes_subflow.py @@ -0,0 +1,29 @@ +import folder_paths +import json +import os.path as osp + +class Subflow: + @classmethod + def INPUT_TYPES(s): + return {"required": { "subflow_name": (folder_paths.get_filename_list("subflows"), ), }} + RETURN_TYPES = () + FUNCTION = "exec_subflow" + + CATEGORY = "loaders" + + def exec_subflow(self, subflow_name): + subflow_path = folder_paths.get_full_path("subflows", subflow_name) + with open(subflow_path) as f: + if osp.splitext(subflow_path)[1] == ".json": + subflow_data = json.load(f) + return subflow_data + + return None + +NODE_CLASS_MAPPINGS = { + "Subflow": Subflow, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Subflow": "Load Subflow" +} diff --git a/folder_paths.py b/folder_paths.py index 4a10c68e7..0516fe199 100644 --- a/folder_paths.py +++ b/folder_paths.py @@ -2,6 +2,7 @@ import os import time supported_pt_extensions = set(['.ckpt', '.pt', '.bin', '.pth', '.safetensors']) +supported_subflow_extensions = set(['.json', '.png']) folder_names_and_paths = {} @@ -29,15 +30,21 @@ folder_names_and_paths["custom_nodes"] = ([os.path.join(base_path, "custom_nodes folder_names_and_paths["hypernetworks"] = ([os.path.join(models_dir, "hypernetworks")], supported_pt_extensions) +folder_names_and_paths["subflows"] = ([os.path.join(base_path, "subflows")], supported_subflow_extensions) + output_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), "output") temp_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), "temp") input_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), "input") +subflows_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)), "subflows") filename_list_cache = {} if not os.path.exists(input_directory): os.makedirs(input_directory) +if not os.path.exists(subflows_directory): + os.makedirs(subflows_directory) + def set_output_directory(output_dir): global output_directory output_directory = output_dir @@ -58,6 +65,9 @@ def get_input_directory(): global input_directory return input_directory +def get_subflows_directory(): + global subflows_directory + return subflows_directory #NOTE: used in http server so don't put folders that should not be accessed remotely def get_directory_by_type(type_name): @@ -67,6 +77,8 @@ def get_directory_by_type(type_name): return get_temp_directory() if type_name == "input": return get_input_directory() + if type_name == "subflows": + return get_subflows_directory() return None @@ -82,6 +94,9 @@ def annotated_filepath(name): elif name.endswith("[temp]"): base_dir = get_temp_directory() name = name[:-7] + elif name.endswith("[subflows]"): + base_dir = get_subflows_directory() + name = name[:-11] else: return name, None diff --git a/nodes.py b/nodes.py index 16bf07cca..a5f6d9818 100644 --- a/nodes.py +++ b/nodes.py @@ -1795,7 +1795,8 @@ def init_custom_nodes(): "nodes_clip_sdxl.py", "nodes_canny.py", "nodes_freelunch.py", - "nodes_custom_sampler.py" + "nodes_custom_sampler.py", + "nodes_subflow.py", ] for node_file in extras_files: diff --git a/server.py b/server.py index 63f337a87..bde01e0dc 100644 --- a/server.py +++ b/server.py @@ -1,4 +1,5 @@ import os +import os.path as osp import sys import asyncio import traceback @@ -153,6 +154,8 @@ class PromptServer(): type_dir = folder_paths.get_temp_directory() elif dir_type == "output": type_dir = folder_paths.get_output_directory() + elif dir_type == "subflows": + type_dir = folder_paths.get_subflows_directory() return type_dir, dir_type @@ -516,6 +519,21 @@ class PromptServer(): return web.Response(status=200) + @routes.get("/subflows/{subflow_name}") + async def get_subflow(request): + subflow_name = request.match_info.get("subflow_name", None) + if subflow_name != None: + subflow_path = folder_paths.get_full_path("subflows", subflow_name) + ext = osp.splitext(subflow_path)[1] + with open(subflow_path) as f: + if ext == ".json": + subflow_data = json.load(f) + return web.json_response({"subflow": subflow_data}, status=200) + elif ext == ".png": + return web.json_response({"error": "todo", "node_errors": []}, status=400) + return web.json_response({"error": "no subflow_name provided", "node_errors": []}, status=400) + + def add_routes(self): self.app.add_routes(self.routes) diff --git a/web/extensions/core/subflow.js b/web/extensions/core/subflow.js new file mode 100644 index 000000000..2a3aa3947 --- /dev/null +++ b/web/extensions/core/subflow.js @@ -0,0 +1,54 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +app.registerExtension({ + name: "Comfy.Subflow", + async nodeCreated(node) { + + if (!node.widgets) return; + if (node.widgets[0].name !== "subflow_name") return; + + const refreshPins = (subflowNodes) => { + // remove all existing pins + const numInputs = node.inputs.length; + const numOutputs = node.outputs.length; + for(let i = numInputs-1; i > -1; i--) { + node.removeInput(i); + } + for(let i = numOutputs-1; i > -1; i--) { + node.removeOutput(i); + } + + for (const subflowNode of subflowNodes) { + const exports = subflowNode.properties.exports; + if (exports) { + for (const inputRef of exports.inputs) { + const input = subflowNode.inputs.find(q => q.name === inputRef); + if (!input) continue; + node.addInput(input.name, input.type); + } + for (const outputRef of exports.outputs) { + const output = subflowNode.outputs.find(q => q.name === outputRef); + if (!output) continue; + node.addOutput(output.name, output.type); + } + } + } + }; + + node.onConfigure = async function () { + const subflowData = await api.getSubflow(node.widgets[0].value); + if (subflowData.subflow) { + refreshPins(subflowData.subflow.nodes); + } + }; + + node.widgets[0].callback = async function (subflowName) { + const subflowData = await api.getSubflow(subflowName); + if (subflowData.subflow) { + refreshPins(subflowData.subflow.nodes); + } + }; + + } +}); diff --git a/web/scripts/api.js b/web/scripts/api.js index b1d245d73..188d7a28d 100644 --- a/web/scripts/api.js +++ b/web/scripts/api.js @@ -264,6 +264,20 @@ class ComfyApi extends EventTarget { } } + /** + * Gets the subflow json data + * @returns Prompt history including node outputs + */ + async getSubflow(subflowName) { + try { + const res = await this.fetchApi(`/subflows/${subflowName}`); + return await res.json(); + } catch (error) { + console.error(error); + return { }; + } + } + /** * Gets system & device stats * @returns System stats such as python version, OS, per device info