From 0ece85d50a57770975b76032a32618d971a1057e Mon Sep 17 00:00:00 2001 From: ad-astra-video <99882368+ad-astra-video@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:46:10 +0000 Subject: [PATCH 01/24] initial commit for comfystream api --- server/api.py | 31 +++++++++++ server/app.py | 3 ++ src/comfystream/server/utils/utils.py | 76 +++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 server/api.py diff --git a/server/api.py b/server/api.py new file mode 100644 index 00000000..789e97bc --- /dev/null +++ b/server/api.py @@ -0,0 +1,31 @@ +from utils import install_node, list_nodes + + +#hiddenswitch comfyui openapi spec +#https://github.com/hiddenswitch/ComfyUI/blob/master/comfy/api/openapi.yaml + +def add_routes(app): + app.router.add_get("/tool/list_nodes", nodes) + app.router.add_post("/tool/install_nodes", install_nodes) + +async def nodes(request): + return await list_nodes(request.app['workspace']) + +async def install_nodes(request): + params = await request.json() + try: + for node in params["nodes"]: + install_node(node, request.app["workspace"]) + + return {"success": True} + except Exception as e: + return {"error": str(e)} + +async def model(request): + pass + +async def add_model(request): + pass + +async def delete_model(request): + pass \ No newline at end of file diff --git a/server/app.py b/server/app.py index a52f8061..3acc8fc8 100644 --- a/server/app.py +++ b/server/app.py @@ -483,4 +483,7 @@ def force_print(*args, **kwargs): if args.comfyui_inference_log_level: app["comfui_inference_log_level"] = args.comfyui_inference_log_level + from api import add_routes + add_routes(app) + web.run_app(app, host=args.host, port=int(args.port), print=force_print) diff --git a/src/comfystream/server/utils/utils.py b/src/comfystream/server/utils/utils.py index c7a7ac30..da4cc182 100644 --- a/src/comfystream/server/utils/utils.py +++ b/src/comfystream/server/utils/utils.py @@ -5,9 +5,14 @@ import types import logging from aiohttp import web +import os +from pathlib import Path + from typing import List, Tuple from contextlib import asynccontextmanager +from git import Repo + logger = logging.getLogger(__name__) @@ -83,3 +88,74 @@ async def temporary_log_level(logger_name: str, level: int): finally: if level is not None: logger.setLevel(original_level) + +def list_nodes(workspace_dir): + custom_nodes_path = Path(os.path.join(workspace_dir, "custom_nodes")) + custom_nodes_path.mkdir(parents=True, exist_ok=True) + os.chdir(custom_nodes_path) + + nodes = [] + for node in custom_nodes_path.iterdir(): + if node.is_dir(): + print(f"checking custom_node:{node.name}") + repo = Repo(node) + fetch_info = repo.remotes.origin.fetch(repo.active_branch.name) + + node_info = { + "name": node.name, + "url": repo.remotes.origin.url, + "branch": repo.active_branch.name, + "commit": repo.head.commit.hexsha[:7], + "update_available": repo.head.commit.hexsha != fetch_info[0].commit.hexsha, + } + + try: + with open(node / "node_info.json") as f: + node_info.update(json.load(f)) + except FileNotFoundError: + pass + + nodes.append(node_info) + + return nodes + + +def install_node(node, workspace_dir): + custom_nodes_path = workspace_dir / "custom_nodes" + custom_nodes_path.mkdir(parents=True, exist_ok=True) + os.chdir(custom_nodes_path) + + try: + dir_name = node_info['url'].split("/")[-1].replace(".git", "") + node_path = custom_nodes_path / dir_name + + print(f"Installing {node_info['name']}...") + + # Clone the repository if it doesn't already exist + if not node_path.exists(): + cmd = ["git", "clone", node_info['url']] + if 'branch' in node_info: + cmd.extend(["-b", node_info['branch']]) + subprocess.run(cmd, check=True) + else: + print(f"{node_info['name']} already exists, skipping clone.") + + # Checkout specific commit if branch is a commit hash + if 'branch' in node_info and len(node_info['branch']) == 40: # SHA-1 hash length + subprocess.run(["git", "-C", dir_name, "checkout", node_info['branch']], check=True) + + # Install requirements if present + requirements_file = node_path / "requirements.txt" + if requirements_file.exists(): + subprocess.run([sys.executable, "-m", "pip", "install", "-r", str(requirements_file)], check=True) + + # Install additional dependencies if specified + if 'dependencies' in node_info: + for dep in node_info['dependencies']: + subprocess.run([sys.executable, "-m", "pip", "install", dep], check=True) + + print(f"Installed {node_info['name']}") + except Exception as e: + print(f"Error installing {node_info['name']} {e}") + raise e + return From 465fec4f70be7506d6c84ea41b57c06d17c1549c Mon Sep 17 00:00:00 2001 From: ad-astra-video <99882368+ad-astra-video@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:46:45 +0000 Subject: [PATCH 02/24] add GitPython to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f94f3345..e708cd4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ toml twilio prometheus_client librosa +GitPython \ No newline at end of file From e42cc31378e042a843513ab5b061e752758578b7 Mon Sep 17 00:00:00 2001 From: Brad P Date: Sat, 8 Feb 2025 05:43:22 +0000 Subject: [PATCH 03/24] Add nodes, models and settings api --- server/api.py | 31 --- server/api/__init__.py | 0 server/api/api.py | 278 ++++++++++++++++++++++++++ server/api/models/__init__.py | 0 server/api/models/models.py | 114 +++++++++++ server/api/nodes/__init__.py | 0 server/api/nodes/nodes.py | 124 ++++++++++++ server/api/settings/__init__.py | 0 server/api/settings/settings.py | 7 + server/app.py | 2 +- src/comfystream/server/utils/utils.py | 4 + 11 files changed, 528 insertions(+), 32 deletions(-) delete mode 100644 server/api.py create mode 100644 server/api/__init__.py create mode 100644 server/api/api.py create mode 100644 server/api/models/__init__.py create mode 100644 server/api/models/models.py create mode 100644 server/api/nodes/__init__.py create mode 100644 server/api/nodes/nodes.py create mode 100644 server/api/settings/__init__.py create mode 100644 server/api/settings/settings.py diff --git a/server/api.py b/server/api.py deleted file mode 100644 index 789e97bc..00000000 --- a/server/api.py +++ /dev/null @@ -1,31 +0,0 @@ -from utils import install_node, list_nodes - - -#hiddenswitch comfyui openapi spec -#https://github.com/hiddenswitch/ComfyUI/blob/master/comfy/api/openapi.yaml - -def add_routes(app): - app.router.add_get("/tool/list_nodes", nodes) - app.router.add_post("/tool/install_nodes", install_nodes) - -async def nodes(request): - return await list_nodes(request.app['workspace']) - -async def install_nodes(request): - params = await request.json() - try: - for node in params["nodes"]: - install_node(node, request.app["workspace"]) - - return {"success": True} - except Exception as e: - return {"error": str(e)} - -async def model(request): - pass - -async def add_model(request): - pass - -async def delete_model(request): - pass \ No newline at end of file diff --git a/server/api/__init__.py b/server/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/api/api.py b/server/api/api.py new file mode 100644 index 00000000..aa0d1eca --- /dev/null +++ b/server/api/api.py @@ -0,0 +1,278 @@ +from pathlib import Path +import os +import json +from git import Repo +import logging +import sys +from aiohttp import web + +from api.nodes.nodes import list_nodes, install_node, delete_node +from api.models.models import list_models, add_model, delete_model +from api.settings.settings import set_twilio_account_info + +def add_routes(app): + app.router.add_get("/env/list_nodes", nodes) + app.router.add_post("/env/install_nodes", install_nodes) + app.router.add_post("/env/delete_nodes", delete_nodes) + + app.router.add_get("/env/list_models", models) + app.router.add_post("/env/add_models", add_models) + app.router.add_post("/env/delete_models", delete_models) + + app.router.add_post("/env/set_account_info", set_account_info) + + +async def nodes(request): + ''' + List all custom nodes in the workspace + + # Example response: + { + "error": null, + "nodes": + [ + { + "name": ComfyUI-Custom-Node, + "version": "0.0.1", + "url": "https://github.com/custom-node-maker/ComfyUI-Custom-Node", + "branch": "main", + "commit": "uasfg98", + "update_available": false, + }, + { + ... + }, + { + ... + } + ] + } + + ''' + workspace_dir = request.app["workspace"] + try: + nodes = await list_nodes(workspace_dir) + return web.json_response({"error": None, "nodes": nodes}) + except Exception as e: + return web.json_response({"error": str(e), "nodes": nodes}, status=500) + +async def install_nodes(request): + ''' + Install ComfyUI custom node from git repository. + + Installs requirements.txt from repository if present + + # Parameters: + url: url of the git repository + branch: branch of the git repository + depdenencies: comma separated list of dependencies to install with pip (optional) + + # Example request: + [ + { + "url": "https://github.com/custom-node-maker/ComfyUI-Custom-Node", + "branch": "main" + }, + { + "url": "https://github.com/custom-node-maker/ComfyUI-Custom-Node", + "branch": "main", + "dependencies": "requests, numpy" + } + ] + ''' + workspace_dir = request.app["workspace"] + try: + nodes = await request.json() + installed_nodes = [] + for node in nodes: + await install_node(node, workspace_dir) + installed_nodes.append(node['url']) + return web.json_response({"success": True, "error": None, "installed_nodes": installed_nodes}) + except Exception as e: + return web.json_response({"success": False, "error": str(e), "installed_nodes": installed_nodes}, status=500) + +async def delete_nodes(request): + ''' + Delete ComfyUI custom node + + # Parameters: + name: name of the repository (e.g. ComfyUI-Custom-Node for url "https://github.com/custom-node-maker/ComfyUI-Custom-Node") + + # Example request: + [ + { + "name": "ComfyUI-Custom-Node" + }, + { + ... + } + ] + ''' + workspace_dir = request.app["workspace"] + try: + nodes = await request.json() + deleted_nodes = [] + for node in nodes: + await delete_node(node, workspace_dir) + deleted_nodes.append(node['name']) + return web.json_response({"success": True, "error": None, "deleted_nodes": deleted_nodes}) + except Exception as e: + return web.json_response({"success": False, "error": str(e), "deleted_nodes": deleted_nodes}, status=500) + +async def models(request): + ''' + List all custom models in the workspace + + # Example response: + { + "error": null, + "models": + { + "checkpoints": [ + { + "name": "dreamshaper-8.safetensors", + "path": "SD1.5/dreamshaper-8.safetensors", + "type": "checkpoint", + "downloading": false" + } + ], + "controlnet": [ + { + "name": "controlnet.sd15.safetensors", + "path": "SD1.5/controlnet.sd15.safetensors", + "type": "controlnet", + "downloading": false" + } + ], + "unet": [ + { + "name": "unet.sd15.safetensors", + "path": "SD1.5/unet.sd15.safetensors", + "type": "unet", + "downloading": false" + } + ], + "vae": [ + { + "name": "vae.safetensors", + "path": "vae.safetensors", + "type": "vae", + "downloading": false" + } + ], + "tensorrt": [ + { + "name": "model.trt", + "path": "model.trt", + "type": "tensorrt", + "downloading": false" + } + ] + } + } + + ''' + workspace_dir = request.app["workspace"] + try: + nodes = await list_models(workspace_dir) + return web.json_response({"error": None, "models": models}) + except Exception as e: + return web.json_response({"error": str(e), "models": models}, status=500) + +async def add_models(request): + ''' + Download models from url + + # Parameters: + url: url of the git repository + type: type of model (e.g. checkpoints, controlnet, unet, vae, onnx, tensorrt) + path: path of the model. supports up to 1 subfolder (e.g. SD1.5/newmodel.safetensors) + + # Example request: + [ + { + "url": "http://url.to/model.safetensors", + "type": "checkpoints" + }, + { + "url": "http://url.to/controlnet.super.safetensors", + "type": "controlnet", + "path": "SD1.5/controlnet.super.safetensors" + } + ] + ''' + workspace_dir = request.app["workspace"] + try: + models = await request.json() + added_models = [] + for model in models: + await add_model(model, workspace_dir) + added_models.append(model['url']) + return web.json_response({"success": True, "error": None, "added_models": added_models}) + except Exception as e: + return web.json_response({"success": False, "error": str(e), "added_nodes": added_models}, status=500) + +async def delete_models(request): + ''' + Delete model + + # Parameters: + type: type of model (e.g. checkpoints, controlnet, unet, vae, onnx, tensorrt) + path: path of the model. supports up to 1 subfolder (e.g. SD1.5/newmodel.safetensors) + + # Example request: + [ + { + "type": "checkpoints", + "path": "model.safetensors" + }, + { + "type": "controlnet", + "path": "SD1.5/controlnet.super.safetensors" + } + ] + ''' + workspace_dir = request.app["workspace"] + try: + models = await request.json() + deleted_models = [] + for model in models: + await delete_model(model, workspace_dir) + deleted_models.append(model['path']) + return web.json_response({"success": True, "error": None, "deleted_models": deleted_models}) + except Exception as e: + return web.json_response({"success": False, "error": str(e), "deleted_models": deleted_models}, status=500) + +async def set_account_info(request): + ''' + Set account info for ice server providers + + # Parameters: + type: account type (e.g. twilio) + account_id: account id from provider + auth_token: auth token from provider + + # Example request: + [ + { + "type": "twilio", + "account_id": "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "auth_token": "your_auth_token" + }, + { + ... + } + ] + + ''' + try: + accounts = await request.json() + accounts_updated = [] + for account in accounts: + if 'type' in account: + if account['type'] == 'twilio': + await set_twilio_account_info(account) + accounts_updated.append(account['type']) + return web.json_response({"success": True, "error": None, "accounts_updated": accounts_updated}) + except Exception as e: + return web.json_response({"success": False, "error": str(e), "accounts_updated": accounts_updated}, status=500) diff --git a/server/api/models/__init__.py b/server/api/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/api/models/models.py b/server/api/models/models.py new file mode 100644 index 00000000..5cf266e7 --- /dev/null +++ b/server/api/models/models.py @@ -0,0 +1,114 @@ +import asyncio +from pathlib import Path +import os +import logging +from aiohttp import ClientSession + +logger = logging.getLogger(__name__) + +async def list_models(workspace_dir): + models_path = Path(os.path.join(workspace_dir, "models")) + models_path.mkdir(parents=True, exist_ok=True) + os.chdir(models_path) + + model_types = ["checkpoints", "controlnet", "unet", "vae", "onnx", "tensorrt"] + + models = {} + for model_type in model_types: + models[model_type] = [] + model_path = models_path / model_type + if not model_path.exists(): + continue + for model in model_path.iterdir(): + if model.is_dir(): + for submodel in model.iterdir(): + if submodel.is_file(): + model_info = { + "name": submodel.name, + "path": f"{model.name}/{submodel.name}", + "type": model_type, + "downloading": os.path.exists(f"{model_type}/{model.name}/{submodel.name}.downloading") + } + models[model_type].append(model_info) + + if model.is_file(): + model_info = { + "name": model.name, + "path": f"{model.name}", + "type": model_type, + "downloading": os.path.exists(f"{model_type}/{model.name}.downloading") + } + models[model_type].append(model_info) + + return models + +async def add_model(model, workspace_dir): + if not 'url' in model: + raise Exception("model url is required") + if not 'type' in model: + raise Exception("model type is required (e.g. checkpoints, controlnet, unet, vae, onnx, tensorrt)") + + try: + model_name = model['url'].split("/")[-1] + model_path = Path(os.path.join(workspace_dir, "models", model['type'], model_name)) + if 'path' in model: + model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path'])) + logger.info(f"model path: {model_path}") + + # check path is in workspace_dir, raises value error if not + model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models"))) + + # start downloading the model in background without blocking + asyncio.create_task(download_model(model['url'], model_path)) + except Exception as e: + os.remove(model_path)+".downloading" + raise Exception(f"error downloading model: {e}") + +async def delete_model(model, workspace_dir): + if not 'type' in model: + raise Exception("model type is required (e.g. checkpoints, controlnet, unet, vae, onnx, tensorrt)") + if not 'path' in model: + raise Exception("model path is required") + try: + model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path'])) + #check path is in workspace_dir, raises value error if not + model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models"))) + + os.remove(model_path) + except Exception as e: + raise Exception(f"error deleting model: {e}") + +async def download_model(url: str, save_path: Path): + try: + temp_file = save_path.with_suffix(save_path.suffix + ".downloading") + + async with ClientSession() as session: + logger.info(f"downloading model from {url} to {save_path}") + # Create empty file to track download in process + model_name = os.path.basename(save_path) + + open(temp_file, "w").close() + + async with session.get(url) as response: + if response.status == 200: + total_size = int(response.headers.get('Content-Length', 0)) + total_downloaded = 0 + last_logged_percentage = -1 # Ensures first log at 1% + with open(save_path, "wb") as f: + while chunk := await response.content.read(4096): # Read in chunks of 1KB + f.write(chunk) + total_downloaded += len(chunk) + # Calculate percentage and log only if it has increased by 1% + percentage = (total_downloaded / total_size) * 100 + if int(percentage) > last_logged_percentage: + last_logged_percentage = int(percentage) + logger.info(f"Downloaded {total_downloaded} of {total_size} bytes ({percentage:.2f}%) of {model_name}") + + #remove download in process file + os.remove(temp_file) + print(f"Model downloaded and saved to {save_path}") + else: + raise print(f"Failed to download model. HTTP Status: {response.status}") + except Exception as e: + #remove download in process file + os.remove(temp_file) \ No newline at end of file diff --git a/server/api/nodes/__init__.py b/server/api/nodes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py new file mode 100644 index 00000000..040924b6 --- /dev/null +++ b/server/api/nodes/nodes.py @@ -0,0 +1,124 @@ +from pathlib import Path +import os +import json +from git import Repo +import logging +import subprocess +import sys +import shutil + +logger = logging.getLogger(__name__) + +async def list_nodes(workspace_dir): + custom_nodes_path = Path(os.path.join(workspace_dir, "custom_nodes")) + custom_nodes_path.mkdir(parents=True, exist_ok=True) + os.chdir(custom_nodes_path) + + nodes = [] + for node in custom_nodes_path.iterdir(): + if node.is_dir(): + logger.info(f"getting info for node: { node.name}") + node_info = { + "name": node.name, + "version": "unknown", + "url": "unknown", + "branch": "unknown", + "commit": "unknown", + "update_available": "unknown", + } + + #include VERSION if set in file + version_file = os.path.join(custom_nodes_path, node.name, "VERSION") + if os.path.exists(version_file): + node_info["version"] = json.dumps(open(version_file).readline().strip()) + + #include git info if available + try: + repo = Repo(node) + node_info["url"] = repo.remotes.origin.url.replace(".git","") + node_info["commit"] = repo.head.commit.hexsha[:7] + if not repo.head.is_detached: + node_info["branch"] = repo.active_branch.name + fetch_info = repo.remotes.origin.fetch(repo.active_branch.name) + node_info["update_available"] = repo.head.commit.hexsha[:7] != fetch_info[0].commit.hexsha[:7] + else: + node_info["branch"] = "detached" + + except Exception as e: + logger.info(f"error getting repo info for {node.name} {e}") + + nodes.append(node_info) + + return nodes + +async def install_node(node, workspace_dir): + ''' + install ComfyUI custom node in git repository. + + installs requirements.txt from repository if present + + paramaters: + url: url of the git repository + branch: branch to install + dependencies: comma separated list of pip dependencies to install + ''' + + custom_nodes_path = Path(os.path.join(workspace_dir, "custom_nodes")) + custom_nodes_path.mkdir(parents=True, exist_ok=True) + os.chdir(custom_nodes_path) + node_url = node.get("url", "") + if node_url == "": + raise ValueError("url is required") + + if not ".git" in node_url: + node_url = f"{node_url}.git" + + try: + dir_name = node_url.split("/")[-1].replace(".git", "") + node_path = custom_nodes_path / dir_name + if not node_path.exists(): + # Clone and install the repository if it doesn't already exist + logger.info(f"installing {dir_name}...") + repo = Repo.clone_from(node["url"], node_path, depth=1) + if "branch" in node: + repo.git.checkout(node['branch']) + else: + # Update the repository if it already exists + logger.info(f"updating node {dir_name}") + repo = Repo(node_path) + repo.remotes.origin.fetch() + branch = node.get("branch", repo.remotes.origin.refs[0].remote_head) + + repo.remotes.origin.pull(branch) + + # Install requirements if present + requirements_file = node_path / "requirements.txt" + if requirements_file.exists(): + subprocess.run([sys.executable, "-m", "pip", "install", "-r", str(requirements_file)], check=True) + + # Install additional dependencies if specified + if "dependencies" in node: + for dep in node["dependencies"].split(','): + subprocess.run([sys.executable, "-m", "pip", "install", dep.strip()], check=True) + + except Exception as e: + logger.error(f"Error installing {dir_name} {e}") + raise e + +async def delete_node(node, workspace_dir): + custom_nodes_path = Path(os.path.join(workspace_dir, "custom_nodes")) + custom_nodes_path.mkdir(parents=True, exist_ok=True) + os.chdir(custom_nodes_path) + if "name" not in node: + raise ValueError("name is required") + + node_path = custom_nodes_path / node["name"] + if not node_path.exists(): + raise ValueError(f"node {node['name']} does not exist") + try: + #delete the folder and all its contents. ignore_errors allows readonly files to be deleted + logger.info(f"deleting node {node['name']}") + shutil.rmtree(node_path, ignore_errors=True) + except Exception as e: + logger.error(f"error deleting node {node['name']}") + raise Exception(f"error deleting node: {e}") diff --git a/server/api/settings/__init__.py b/server/api/settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/api/settings/settings.py b/server/api/settings/settings.py new file mode 100644 index 00000000..253bd012 --- /dev/null +++ b/server/api/settings/settings.py @@ -0,0 +1,7 @@ +import os + +async def set_twilio_account_info(account_sid, auth_token): + if not account_sid is None: + os.environ["TWILIO_ACCOUNT_SID"] = account_sid + if not auth_token is None: + os.environ["TWILIO_AUTH_TOKEN"] = auth_token diff --git a/server/app.py b/server/app.py index 3acc8fc8..7e70ddd6 100644 --- a/server/app.py +++ b/server/app.py @@ -483,7 +483,7 @@ def force_print(*args, **kwargs): if args.comfyui_inference_log_level: app["comfui_inference_log_level"] = args.comfyui_inference_log_level - from api import add_routes + from api.api import add_routes add_routes(app) web.run_app(app, host=args.host, port=int(args.port), print=force_print) diff --git a/src/comfystream/server/utils/utils.py b/src/comfystream/server/utils/utils.py index da4cc182..b4189f59 100644 --- a/src/comfystream/server/utils/utils.py +++ b/src/comfystream/server/utils/utils.py @@ -1,12 +1,16 @@ """General utility functions.""" import asyncio +import json import random import types import logging from aiohttp import web import os from pathlib import Path +import subprocess +import sys +import requests from typing import List, Tuple from contextlib import asynccontextmanager From bd42ec5639a46e045f0338bb697b7c47845c0f82 Mon Sep 17 00:00:00 2001 From: Brad P Date: Sat, 8 Feb 2025 06:58:06 +0000 Subject: [PATCH 04/24] install nodes in conda environments --- server/api/api.py | 2 +- server/api/nodes/nodes.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/api/api.py b/server/api/api.py index aa0d1eca..2541bae4 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -174,7 +174,7 @@ async def models(request): ''' workspace_dir = request.app["workspace"] try: - nodes = await list_models(workspace_dir) + models = await list_models(workspace_dir) return web.json_response({"error": None, "models": models}) except Exception as e: return web.json_response({"error": str(e), "models": models}, status=500) diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py index 040924b6..f8b79084 100644 --- a/server/api/nodes/nodes.py +++ b/server/api/nodes/nodes.py @@ -94,12 +94,14 @@ async def install_node(node, workspace_dir): # Install requirements if present requirements_file = node_path / "requirements.txt" if requirements_file.exists(): - subprocess.run([sys.executable, "-m", "pip", "install", "-r", str(requirements_file)], check=True) + subprocess.run(["conda", "run", "-n", "comfystream", "pip", "install", "-r", str(requirements_file)], check=True) + subprocess.run(["conda", "run", "-n", "comfyui", "pip", "install", "-r", str(requirements_file)], check=True) # Install additional dependencies if specified if "dependencies" in node: for dep in node["dependencies"].split(','): - subprocess.run([sys.executable, "-m", "pip", "install", dep.strip()], check=True) + subprocess.run(["conda", "run", "-n", "comfystream", "pip", "install", dep.strip()], check=True) + subprocess.run(["conda", "run", "-n", "comfyui", "pip", "install", dep.strip()], check=True) except Exception as e: logger.error(f"Error installing {dir_name} {e}") From 9d8e95bd5395bea29b6bebbbc0f53f05044907c6 Mon Sep 17 00:00:00 2001 From: Brad P Date: Thu, 13 Feb 2025 17:35:27 +0000 Subject: [PATCH 05/24] fixes for models download and nodes listing --- server/api/models/models.py | 4 ++-- server/api/nodes/nodes.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/server/api/models/models.py b/server/api/models/models.py index 5cf266e7..447b9486 100644 --- a/server/api/models/models.py +++ b/server/api/models/models.py @@ -57,7 +57,7 @@ async def add_model(model, workspace_dir): # check path is in workspace_dir, raises value error if not model_path.resolve().relative_to(Path(os.path.join(workspace_dir, "models"))) - + os.makedirs(model_path.parent, exist_ok=True) # start downloading the model in background without blocking asyncio.create_task(download_model(model['url'], model_path)) except Exception as e: @@ -81,7 +81,7 @@ async def delete_model(model, workspace_dir): async def download_model(url: str, save_path: Path): try: temp_file = save_path.with_suffix(save_path.suffix + ".downloading") - + print("downloading") async with ClientSession() as session: logger.info(f"downloading model from {url} to {save_path}") # Create empty file to track download in process diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py index f8b79084..4375a969 100644 --- a/server/api/nodes/nodes.py +++ b/server/api/nodes/nodes.py @@ -16,6 +16,9 @@ async def list_nodes(workspace_dir): nodes = [] for node in custom_nodes_path.iterdir(): + if node.name == "__pycache__": + continue + if node.is_dir(): logger.info(f"getting info for node: { node.name}") node_info = { @@ -57,7 +60,7 @@ async def install_node(node, workspace_dir): installs requirements.txt from repository if present - paramaters: + # Paramaters url: url of the git repository branch: branch to install dependencies: comma separated list of pip dependencies to install From 020b9d983abe8249a7f62778616e1b5fc728ed84 Mon Sep 17 00:00:00 2001 From: Brad P Date: Thu, 13 Feb 2025 21:32:56 +0000 Subject: [PATCH 06/24] reload embedded client --- server/api/api.py | 61 +++++++++++++++++++++++++++++++++++---- server/api/nodes/nodes.py | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 6 deletions(-) diff --git a/server/api/api.py b/server/api/api.py index 2541bae4..cf2d0a5c 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -1,15 +1,17 @@ -from pathlib import Path -import os -import json -from git import Repo -import logging -import sys from aiohttp import web from api.nodes.nodes import list_nodes, install_node, delete_node from api.models.models import list_models, add_model, delete_model from api.settings.settings import set_twilio_account_info +from comfy.nodes.package_typing import ExportedNodes +from comfy.nodes.package import _comfy_nodes, import_all_nodes_in_workspace +from comfy.cmd.execution import nodes + +from api.nodes.nodes import force_import_all_nodes_in_workspace +#use a different node import +import_all_nodes_in_workspace = force_import_all_nodes_in_workspace + def add_routes(app): app.router.add_get("/env/list_nodes", nodes) app.router.add_post("/env/install_nodes", install_nodes) @@ -19,9 +21,38 @@ def add_routes(app): app.router.add_post("/env/add_models", add_models) app.router.add_post("/env/delete_models", delete_models) + app.router.add_post("/env/reload", reload) app.router.add_post("/env/set_account_info", set_account_info) +async def reload(request): + ''' + Reload ComfyUI environment + ''' + + #reset embedded client + from comfy.client.embedded_comfy_client import EmbeddedComfyClient + from comfy.cli_args_types import Configuration + await request.app["pipeline"].client.comfy_client.__aexit__() + + #reset imports to clear imported nodes + global _comfy_nodes + if len(_comfy_nodes) > 0: + import sys + import importlib + del sys.modules['comfy.nodes.package'] + del sys.modules['comfy.cmd.execution'] + globals()['comfy.nodes.package'] = importlib.import_module('comfy.nodes.package') + globals()['comfy.cmd.execution'] = importlib.import_module('comfy.cmd.execution') + #use a different node import + import_all_nodes_in_workspace = force_import_all_nodes_in_workspace + _comfy_nodes = import_all_nodes_in_workspace() + + #load new embedded client + request.app["pipeline"].client.comfy_client = EmbeddedComfyClient(Configuration(cwd=request.app["workspace"], disable_cuda_malloc=True, gpu_only=True)) + + return web.json_response({"success": True, "error": None}) + async def nodes(request): ''' List all custom nodes in the workspace @@ -87,6 +118,24 @@ async def install_nodes(request): for node in nodes: await install_node(node, workspace_dir) installed_nodes.append(node['url']) + + #restart embedded client + #request.app["pipeline"].client.set_prompt(request.app["pipeline"].client.prompt) + + # reimport nodes to workspace + #from comfy.cmd.execution import nodes + #from comfy.nodes.package import import_all_nodes_in_workspace + #update_nodes = import_all_nodes_in_workspace() + #nodes.update(update_nodes) + + #config = request.app["pipeline"].client.comfy_client._configuration + #from comfy.client.embedded_comfy_client import EmbeddedComfyClient + #request.app["pipeline"].client.comfy_client = EmbeddedComfyClient(config) + #await request.app["pipeline"].warm() + + #from pipeline import Pipeline + #request.app["pipeline"] = Pipeline(cwd=request.app["workspace"], disable_cuda_malloc=True, gpu_only=True) + return web.json_response({"success": True, "error": None, "installed_nodes": installed_nodes}) except Exception as e: return web.json_response({"success": False, "error": str(e), "installed_nodes": installed_nodes}, status=500) diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py index 4375a969..b205f7c8 100644 --- a/server/api/nodes/nodes.py +++ b/server/api/nodes/nodes.py @@ -127,3 +127,59 @@ async def delete_node(node, workspace_dir): except Exception as e: logger.error(f"error deleting node {node['name']}") raise Exception(f"error deleting node: {e}") + + +from comfy.nodes.package import ExportedNodes +from comfy.nodes.package import _comfy_nodes, _import_and_enumerate_nodes_in_module +from functools import reduce +from importlib.metadata import entry_points +import types + +def force_import_all_nodes_in_workspace(vanilla_custom_nodes=True, raise_on_failure=False) -> ExportedNodes: + # now actually import the nodes, to improve control of node loading order + from comfy_extras import nodes as comfy_extras_nodes # pylint: disable=absolute-import-used + from comfy.cli_args import args + from comfy.nodes import base_nodes + from comfy.nodes.vanilla_node_importing import mitigated_import_of_vanilla_custom_nodes + + # only load these nodes once + + base_and_extra = reduce(lambda x, y: x.update(y), + map(lambda module_inner: _import_and_enumerate_nodes_in_module(module_inner, raise_on_failure=raise_on_failure), [ + # this is the list of default nodes to import + base_nodes, + comfy_extras_nodes + ]), + ExportedNodes()) + custom_nodes_mappings = ExportedNodes() + + if args.disable_all_custom_nodes: + logging.info("Loading custom nodes was disabled, only base and extra nodes were loaded") + _comfy_nodes.update(base_and_extra) + return _comfy_nodes + + # load from entrypoints + for entry_point in entry_points().select(group='comfyui.custom_nodes'): + # Load the module associated with the current entry point + try: + module = entry_point.load() + except ModuleNotFoundError as module_not_found_error: + logging.error(f"A module was not found while importing nodes via an entry point: {entry_point}. Please ensure the entry point in setup.py is named correctly", exc_info=module_not_found_error) + continue + + # Ensure that what we've loaded is indeed a module + if isinstance(module, types.ModuleType): + custom_nodes_mappings.update( + _import_and_enumerate_nodes_in_module(module, print_import_times=True)) + + # load the vanilla custom nodes last + if vanilla_custom_nodes: + custom_nodes_mappings += mitigated_import_of_vanilla_custom_nodes() + + # don't allow custom nodes to overwrite base nodes + custom_nodes_mappings -= base_and_extra + + _comfy_nodes.update(base_and_extra + custom_nodes_mappings) + + return _comfy_nodes + From 1171ae791d370734620e1a628a99f0fddb882113 Mon Sep 17 00:00:00 2001 From: Brad P Date: Wed, 12 Mar 2025 20:52:07 +0000 Subject: [PATCH 07/24] update to install in one env --- server/api/nodes/nodes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py index b205f7c8..dcdb907a 100644 --- a/server/api/nodes/nodes.py +++ b/server/api/nodes/nodes.py @@ -98,13 +98,11 @@ async def install_node(node, workspace_dir): requirements_file = node_path / "requirements.txt" if requirements_file.exists(): subprocess.run(["conda", "run", "-n", "comfystream", "pip", "install", "-r", str(requirements_file)], check=True) - subprocess.run(["conda", "run", "-n", "comfyui", "pip", "install", "-r", str(requirements_file)], check=True) # Install additional dependencies if specified if "dependencies" in node: for dep in node["dependencies"].split(','): subprocess.run(["conda", "run", "-n", "comfystream", "pip", "install", dep.strip()], check=True) - subprocess.run(["conda", "run", "-n", "comfyui", "pip", "install", dep.strip()], check=True) except Exception as e: logger.error(f"Error installing {dir_name} {e}") From 678429e83d1573121363a1ca41c5f45bdea2a8c5 Mon Sep 17 00:00:00 2001 From: Brad P Date: Thu, 13 Mar 2025 15:29:49 +0000 Subject: [PATCH 08/24] move adding mgmt api routes before route prefix --- server/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/app.py b/server/app.py index 7e70ddd6..959f7c15 100644 --- a/server/app.py +++ b/server/app.py @@ -468,6 +468,10 @@ async def on_shutdown(app: web.Application): ) app.router.add_get("/metrics", app["metrics_manager"].metrics_handler) + #add management api routes + from api.api import add_routes + add_routes(app) + # Add hosted platform route prefix. # NOTE: This ensures that the local and hosted experiences have consistent routes. add_prefix_to_app_routes(app, "/live") @@ -483,7 +487,5 @@ def force_print(*args, **kwargs): if args.comfyui_inference_log_level: app["comfui_inference_log_level"] = args.comfyui_inference_log_level - from api.api import add_routes - add_routes(app) web.run_app(app, host=args.host, port=int(args.port), print=force_print) From e5d99a60fed0aaac99555a5196e733c372b3f057 Mon Sep 17 00:00:00 2001 From: Brad P Date: Thu, 13 Mar 2025 22:35:50 +0000 Subject: [PATCH 09/24] update reload pipeline --- server/api/api.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/server/api/api.py b/server/api/api.py index cf2d0a5c..4efffbfa 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -3,6 +3,7 @@ from api.nodes.nodes import list_nodes, install_node, delete_node from api.models.models import list_models, add_model, delete_model from api.settings.settings import set_twilio_account_info +from pipeline import Pipeline from comfy.nodes.package_typing import ExportedNodes from comfy.nodes.package import _comfy_nodes, import_all_nodes_in_workspace @@ -33,23 +34,15 @@ async def reload(request): #reset embedded client from comfy.client.embedded_comfy_client import EmbeddedComfyClient from comfy.cli_args_types import Configuration - await request.app["pipeline"].client.comfy_client.__aexit__() + await request.app["pipeline"].cleanup() #reset imports to clear imported nodes global _comfy_nodes - if len(_comfy_nodes) > 0: - import sys - import importlib - del sys.modules['comfy.nodes.package'] - del sys.modules['comfy.cmd.execution'] - globals()['comfy.nodes.package'] = importlib.import_module('comfy.nodes.package') - globals()['comfy.cmd.execution'] = importlib.import_module('comfy.cmd.execution') - #use a different node import - import_all_nodes_in_workspace = force_import_all_nodes_in_workspace - _comfy_nodes = import_all_nodes_in_workspace() - - #load new embedded client - request.app["pipeline"].client.comfy_client = EmbeddedComfyClient(Configuration(cwd=request.app["workspace"], disable_cuda_malloc=True, gpu_only=True)) + import_all_nodes_in_workspace = force_import_all_nodes_in_workspace + _comfy_nodes = import_all_nodes_in_workspace() + + #reload pipeline + request.app["pipeline"] = Pipeline(cwd=request.app["workspace"], disable_cuda_malloc=True, gpu_only=True) return web.json_response({"success": True, "error": None}) From 0988908d1695b766db0257567d4b4a36771f3cd7 Mon Sep 17 00:00:00 2001 From: Brad P Date: Fri, 14 Mar 2025 14:43:52 +0000 Subject: [PATCH 10/24] reset webrtc connections on reload --- server/api/api.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/api/api.py b/server/api/api.py index 4efffbfa..6047b807 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -1,4 +1,5 @@ from aiohttp import web +import asyncio from api.nodes.nodes import list_nodes, install_node, delete_node from api.models.models import list_models, add_model, delete_model @@ -43,7 +44,13 @@ async def reload(request): #reload pipeline request.app["pipeline"] = Pipeline(cwd=request.app["workspace"], disable_cuda_malloc=True, gpu_only=True) - + + #reset webrtc connections + pcs = request.app["pcs"] + coros = [pc.close() for pc in pcs] + await asyncio.gather(*coros) + pcs.clear() + return web.json_response({"success": True, "error": None}) async def nodes(request): From 98f5a6c14f2aa1e0ea7a0b4eb718853a52ec7e2d Mon Sep 17 00:00:00 2001 From: Brad P Date: Fri, 14 Mar 2025 15:01:12 +0000 Subject: [PATCH 11/24] cleanup --- server/api/api.py | 21 --------------------- server/api/nodes/nodes.py | 1 - 2 files changed, 22 deletions(-) diff --git a/server/api/api.py b/server/api/api.py index 6047b807..4b22da60 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -6,9 +6,7 @@ from api.settings.settings import set_twilio_account_info from pipeline import Pipeline -from comfy.nodes.package_typing import ExportedNodes from comfy.nodes.package import _comfy_nodes, import_all_nodes_in_workspace -from comfy.cmd.execution import nodes from api.nodes.nodes import force_import_all_nodes_in_workspace #use a different node import @@ -33,8 +31,6 @@ async def reload(request): ''' #reset embedded client - from comfy.client.embedded_comfy_client import EmbeddedComfyClient - from comfy.cli_args_types import Configuration await request.app["pipeline"].cleanup() #reset imports to clear imported nodes @@ -119,23 +115,6 @@ async def install_nodes(request): await install_node(node, workspace_dir) installed_nodes.append(node['url']) - #restart embedded client - #request.app["pipeline"].client.set_prompt(request.app["pipeline"].client.prompt) - - # reimport nodes to workspace - #from comfy.cmd.execution import nodes - #from comfy.nodes.package import import_all_nodes_in_workspace - #update_nodes = import_all_nodes_in_workspace() - #nodes.update(update_nodes) - - #config = request.app["pipeline"].client.comfy_client._configuration - #from comfy.client.embedded_comfy_client import EmbeddedComfyClient - #request.app["pipeline"].client.comfy_client = EmbeddedComfyClient(config) - #await request.app["pipeline"].warm() - - #from pipeline import Pipeline - #request.app["pipeline"] = Pipeline(cwd=request.app["workspace"], disable_cuda_malloc=True, gpu_only=True) - return web.json_response({"success": True, "error": None, "installed_nodes": installed_nodes}) except Exception as e: return web.json_response({"success": False, "error": str(e), "installed_nodes": installed_nodes}, status=500) diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py index dcdb907a..8aab1b36 100644 --- a/server/api/nodes/nodes.py +++ b/server/api/nodes/nodes.py @@ -4,7 +4,6 @@ from git import Repo import logging import subprocess -import sys import shutil logger = logging.getLogger(__name__) From 5d469365209c665dfa3c419c311489842067bc21 Mon Sep 17 00:00:00 2001 From: Brad P Date: Mon, 17 Mar 2025 20:49:58 +0000 Subject: [PATCH 12/24] cleanup --- server/api/api.py | 2 +- server/app.py | 4 +- src/comfystream/server/utils/utils.py | 70 --------------------------- 3 files changed, 3 insertions(+), 73 deletions(-) diff --git a/server/api/api.py b/server/api/api.py index 4b22da60..228bfee7 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -12,7 +12,7 @@ #use a different node import import_all_nodes_in_workspace = force_import_all_nodes_in_workspace -def add_routes(app): +def add_mgmt_api_routes(app): app.router.add_get("/env/list_nodes", nodes) app.router.add_post("/env/install_nodes", install_nodes) app.router.add_post("/env/delete_nodes", delete_nodes) diff --git a/server/app.py b/server/app.py index 959f7c15..e0921aae 100644 --- a/server/app.py +++ b/server/app.py @@ -469,8 +469,8 @@ async def on_shutdown(app: web.Application): app.router.add_get("/metrics", app["metrics_manager"].metrics_handler) #add management api routes - from api.api import add_routes - add_routes(app) + from api.api import add_mgmt_api_routes + add_mgmt_api_routes(app) # Add hosted platform route prefix. # NOTE: This ensures that the local and hosted experiences have consistent routes. diff --git a/src/comfystream/server/utils/utils.py b/src/comfystream/server/utils/utils.py index b4189f59..ef979cc1 100644 --- a/src/comfystream/server/utils/utils.py +++ b/src/comfystream/server/utils/utils.py @@ -92,74 +92,4 @@ async def temporary_log_level(logger_name: str, level: int): finally: if level is not None: logger.setLevel(original_level) - -def list_nodes(workspace_dir): - custom_nodes_path = Path(os.path.join(workspace_dir, "custom_nodes")) - custom_nodes_path.mkdir(parents=True, exist_ok=True) - os.chdir(custom_nodes_path) - - nodes = [] - for node in custom_nodes_path.iterdir(): - if node.is_dir(): - print(f"checking custom_node:{node.name}") - repo = Repo(node) - fetch_info = repo.remotes.origin.fetch(repo.active_branch.name) - - node_info = { - "name": node.name, - "url": repo.remotes.origin.url, - "branch": repo.active_branch.name, - "commit": repo.head.commit.hexsha[:7], - "update_available": repo.head.commit.hexsha != fetch_info[0].commit.hexsha, - } - try: - with open(node / "node_info.json") as f: - node_info.update(json.load(f)) - except FileNotFoundError: - pass - - nodes.append(node_info) - - return nodes - - -def install_node(node, workspace_dir): - custom_nodes_path = workspace_dir / "custom_nodes" - custom_nodes_path.mkdir(parents=True, exist_ok=True) - os.chdir(custom_nodes_path) - - try: - dir_name = node_info['url'].split("/")[-1].replace(".git", "") - node_path = custom_nodes_path / dir_name - - print(f"Installing {node_info['name']}...") - - # Clone the repository if it doesn't already exist - if not node_path.exists(): - cmd = ["git", "clone", node_info['url']] - if 'branch' in node_info: - cmd.extend(["-b", node_info['branch']]) - subprocess.run(cmd, check=True) - else: - print(f"{node_info['name']} already exists, skipping clone.") - - # Checkout specific commit if branch is a commit hash - if 'branch' in node_info and len(node_info['branch']) == 40: # SHA-1 hash length - subprocess.run(["git", "-C", dir_name, "checkout", node_info['branch']], check=True) - - # Install requirements if present - requirements_file = node_path / "requirements.txt" - if requirements_file.exists(): - subprocess.run([sys.executable, "-m", "pip", "install", "-r", str(requirements_file)], check=True) - - # Install additional dependencies if specified - if 'dependencies' in node_info: - for dep in node_info['dependencies']: - subprocess.run([sys.executable, "-m", "pip", "install", dep], check=True) - - print(f"Installed {node_info['name']}") - except Exception as e: - print(f"Error installing {node_info['name']} {e}") - raise e - return From 79b10c64336db380596ea0d267837f6a690313ca Mon Sep 17 00:00:00 2001 From: Brad P Date: Mon, 17 Mar 2025 21:55:21 +0000 Subject: [PATCH 13/24] update routes --- server/api/api.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server/api/api.py b/server/api/api.py index 228bfee7..50d5ff70 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -13,16 +13,16 @@ import_all_nodes_in_workspace = force_import_all_nodes_in_workspace def add_mgmt_api_routes(app): - app.router.add_get("/env/list_nodes", nodes) - app.router.add_post("/env/install_nodes", install_nodes) - app.router.add_post("/env/delete_nodes", delete_nodes) + app.router.add_get("/settings/nodes/list", nodes) + app.router.add_post("/settings/nodes/install", install_nodes) + app.router.add_post("/settings/nodes/delete", delete_nodes) - app.router.add_get("/env/list_models", models) - app.router.add_post("/env/add_models", add_models) - app.router.add_post("/env/delete_models", delete_models) + app.router.add_get("/settings/models/list", models) + app.router.add_post("/settings/models/add", add_models) + app.router.add_post("/settings/models/delete", delete_models) - app.router.add_post("/env/reload", reload) - app.router.add_post("/env/set_account_info", set_account_info) + app.router.add_post("/settings/reload", reload) + app.router.add_post("/settings/twilio/set_account_info", set_account_info) async def reload(request): ''' From c0c3a92a4f973292d854a64e57ef2332193ebe7c Mon Sep 17 00:00:00 2001 From: Brad P Date: Tue, 18 Mar 2025 01:35:54 +0000 Subject: [PATCH 14/24] update set twilio account info --- server/api/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/api.py b/server/api/api.py index 50d5ff70..b4f9a2ef 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -22,7 +22,7 @@ def add_mgmt_api_routes(app): app.router.add_post("/settings/models/delete", delete_models) app.router.add_post("/settings/reload", reload) - app.router.add_post("/settings/twilio/set_account_info", set_account_info) + app.router.add_post("/settings/twilio/set/account", set_account_info) async def reload(request): ''' From 0c5a05c1e4048798b4239cec23c0004df844b59d Mon Sep 17 00:00:00 2001 From: Brad P Date: Tue, 18 Mar 2025 01:41:10 +0000 Subject: [PATCH 15/24] cleanup --- src/comfystream/server/utils/utils.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/comfystream/server/utils/utils.py b/src/comfystream/server/utils/utils.py index ef979cc1..aaff9f1b 100644 --- a/src/comfystream/server/utils/utils.py +++ b/src/comfystream/server/utils/utils.py @@ -1,22 +1,14 @@ """General utility functions.""" import asyncio -import json import random import types import logging from aiohttp import web -import os -from pathlib import Path -import subprocess -import sys -import requests from typing import List, Tuple from contextlib import asynccontextmanager -from git import Repo - logger = logging.getLogger(__name__) From 1adc535e675d9bd7aa4c5015bdb03dcfe76b7060 Mon Sep 17 00:00:00 2001 From: Brad P Date: Tue, 18 Mar 2025 01:41:33 +0000 Subject: [PATCH 16/24] cleanup --- src/comfystream/server/utils/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/comfystream/server/utils/utils.py b/src/comfystream/server/utils/utils.py index aaff9f1b..716ce333 100644 --- a/src/comfystream/server/utils/utils.py +++ b/src/comfystream/server/utils/utils.py @@ -66,7 +66,6 @@ def add_prefix_to_app_routes(app: web.Application, prefix: str): new_path = prefix + route.resource.canonical app.router.add_route(route.method, new_path, route.handler) - @asynccontextmanager async def temporary_log_level(logger_name: str, level: int): """Temporarily set the log level of a logger. From 4cb6b6773e7c96ecae4b04368b94d00eb30c9b16 Mon Sep 17 00:00:00 2001 From: Brad P Date: Tue, 18 Mar 2025 16:43:11 -0500 Subject: [PATCH 17/24] route update and log lines updates --- server/api/api.py | 2 +- server/api/models/models.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/api/api.py b/server/api/api.py index b4f9a2ef..713e4cf9 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -21,7 +21,7 @@ def add_mgmt_api_routes(app): app.router.add_post("/settings/models/add", add_models) app.router.add_post("/settings/models/delete", delete_models) - app.router.add_post("/settings/reload", reload) + app.router.add_post("/settings/comfystream/reload", reload) app.router.add_post("/settings/twilio/set/account", set_account_info) async def reload(request): diff --git a/server/api/models/models.py b/server/api/models/models.py index 447b9486..2fa11158 100644 --- a/server/api/models/models.py +++ b/server/api/models/models.py @@ -106,9 +106,10 @@ async def download_model(url: str, save_path: Path): #remove download in process file os.remove(temp_file) - print(f"Model downloaded and saved to {save_path}") + logger.info(f"Model downloaded and saved to {save_path}") else: raise print(f"Failed to download model. HTTP Status: {response.status}") except Exception as e: #remove download in process file + logger.error(f"error downloading model: {str(e)}") os.remove(temp_file) \ No newline at end of file From 5825c82072389347fa1db0824e639d7d52a07a9a Mon Sep 17 00:00:00 2001 From: Brad P Date: Sat, 29 Mar 2025 06:33:15 -0500 Subject: [PATCH 18/24] add enable/disable and restart comfyui --- server/api/api.py | 88 ++++++++++++++++++++++++++++++++- server/api/nodes/nodes.py | 55 +++++++++++++++++++++ server/api/settings/settings.py | 7 +++ 3 files changed, 148 insertions(+), 2 deletions(-) diff --git a/server/api/api.py b/server/api/api.py index 713e4cf9..1e6f1339 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -1,9 +1,9 @@ from aiohttp import web import asyncio -from api.nodes.nodes import list_nodes, install_node, delete_node +from api.nodes.nodes import list_nodes, install_node, delete_node, enable_node, disable_node from api.models.models import list_models, add_model, delete_model -from api.settings.settings import set_twilio_account_info +from api.settings.settings import set_twilio_account_info, restart_comfyui from pipeline import Pipeline from comfy.nodes.package import _comfy_nodes, import_all_nodes_in_workspace @@ -16,14 +16,18 @@ def add_mgmt_api_routes(app): app.router.add_get("/settings/nodes/list", nodes) app.router.add_post("/settings/nodes/install", install_nodes) app.router.add_post("/settings/nodes/delete", delete_nodes) + app.router.add_post("/settings/nodes/enable", enable_nodes) + app.router.add_post("/settings/nodes/disable", disable_nodes) app.router.add_get("/settings/models/list", models) app.router.add_post("/settings/models/add", add_models) app.router.add_post("/settings/models/delete", delete_models) app.router.add_post("/settings/comfystream/reload", reload) + app.router.add_post("/settings/comfyui/restart", restart_comfyui_process) app.router.add_post("/settings/twilio/set/account", set_account_info) + async def reload(request): ''' Reload ComfyUI environment @@ -147,6 +151,62 @@ async def delete_nodes(request): except Exception as e: return web.json_response({"success": False, "error": str(e), "deleted_nodes": deleted_nodes}, status=500) +async def enable_nodes(request): + ''' + Enable ComfyUI custom node + + # Parameters: + name: name of the node (e.g. ComfyUI-Custom-Node) + + # Example request: + [ + { + "name": "ComfyUI-Custom-Node" + }, + { + ... + } + ] + ''' + workspace_dir = request.app["workspace"] + try: + nodes = await request.json() + enabled_nodes = [] + for node in nodes: + await enable_node(node, workspace_dir) + enabled_nodes.append(node['name']) + return web.json_response({"success": True, "error": None, "enabled_nodes": enabled_nodes}) + except Exception as e: + return web.json_response({"success": False, "error": str(e), "enabled_nodes": enabled_nodes}, status=500) + +async def disable_nodes(request): + ''' + Disable ComfyUI custom node + + # Parameters: + name: name of the node (e.g. ComfyUI-Custom-Node) + + # Example request: + [ + { + "name": "ComfyUI-Custom-Node" + }, + { + ... + } + ] + ''' + workspace_dir = request.app["workspace"] + try: + nodes = await request.json() + disabled_nodes = [] + for node in nodes: + await disable_node(node, workspace_dir) + disabled_nodes.append(node['name']) + return web.json_response({"success": True, "error": None, "disabled_nodes": disabled_nodes}) + except Exception as e: + return web.json_response({"success": False, "error": str(e), "disabled_nodes": disabled_nodes}, status=500) + async def models(request): ''' List all custom models in the workspace @@ -304,3 +364,27 @@ async def set_account_info(request): return web.json_response({"success": True, "error": None, "accounts_updated": accounts_updated}) except Exception as e: return web.json_response({"success": False, "error": str(e), "accounts_updated": accounts_updated}, status=500) + +async def restart_comfyui_process(request): + ''' + Restart comfyui process + + # Parameters: + None + + # Example request: + [ + { + "restart": "comfyui", + } + ] + + ''' + print("restarting comfyui process") + try: + restart_process = await request.json() + if restart_process["restart"] == "comfyui": + await restart_comfyui(request.app["workspace"]) + return web.json_response({"success": True, "error": None}) + except Exception as e: + return web.json_response({"success": False, "error": str(e)}, status=500) \ No newline at end of file diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py index 8aab1b36..39eaa772 100644 --- a/server/api/nodes/nodes.py +++ b/server/api/nodes/nodes.py @@ -125,6 +125,61 @@ async def delete_node(node, workspace_dir): logger.error(f"error deleting node {node['name']}") raise Exception(f"error deleting node: {e}") +async def enable_node(node, workspace_dir): + custom_nodes_path = Path(os.path.join(workspace_dir, "custom_nodes")) + custom_nodes_path.mkdir(parents=True, exist_ok=True) + os.chdir(custom_nodes_path) + if "name" not in node: + raise ValueError("name is required") + + node_path = custom_nodes_path / node["name"] + #check if enabled node exists + if not node_path.exists(): + #try with .disabled + node_path = custom_nodes_path / node["name"]+".disabled" + if not node_path.exists(): + #node does not exist as enabled or disabled + raise ValueError(f"node {node['name']} does not exist") + else: + #enable the disabled node + try: + #rename the folder to remove .disabled + logger.info(f"enabling node {node['name']}") + node_path.rename(custom_nodes_path / node["name"]) + except Exception as e: + logger.error(f"error enabling node {node['name']}") + raise Exception(f"error enabling node: {e}") + else: + #node is already enabled, nothing to do + return + +async def disable_node(node, workspace_dir): + custom_nodes_path = Path(os.path.join(workspace_dir, "custom_nodes")) + custom_nodes_path.mkdir(parents=True, exist_ok=True) + os.chdir(custom_nodes_path) + if "name" not in node: + raise ValueError("name is required") + + node_path = custom_nodes_path / node["name"] + if not node_path.exists(): + #try with .disabled + node_path = custom_nodes_path / node["name"]+".disabled" + if not node_path.exists(): + #node does not exist as enabled or disabled + raise ValueError(f"node {node['name']} does not exist") + else: + #node is already disabled, nothing to do + return + else: + #rename the folder to add .disabled + try: + #delete the folder and all its contents. ignore_errors allows readonly files to be deleted + logger.info(f"enabling node {node['name']}") + node_path.rename(custom_nodes_path / node["name"]+".disabled") + except Exception as e: + logger.error(f"error enabling node {node['name']}") + raise Exception(f"error enabling node: {e}") + from comfy.nodes.package import ExportedNodes from comfy.nodes.package import _comfy_nodes, _import_and_enumerate_nodes_in_module diff --git a/server/api/settings/settings.py b/server/api/settings/settings.py index 253bd012..6cf40824 100644 --- a/server/api/settings/settings.py +++ b/server/api/settings/settings.py @@ -1,7 +1,14 @@ import os +from pathlib import Path +import psutil +import subprocess +import sys async def set_twilio_account_info(account_sid, auth_token): if not account_sid is None: os.environ["TWILIO_ACCOUNT_SID"] = account_sid if not auth_token is None: os.environ["TWILIO_AUTH_TOKEN"] = auth_token + +async def restart_comfyui(workspace_dir): + subprocess.run(["supervisorctl", "restart", "comfyui"], check=False) From 04b07faf0bc8a265b73c6e7f6414c2e5bea3ff3c Mon Sep 17 00:00:00 2001 From: Brad P Date: Sat, 29 Mar 2025 06:33:40 -0500 Subject: [PATCH 19/24] add restart comfyui to comfystream menu --- nodes/api/__init__.py | 5 +++++ nodes/web/js/launcher.js | 44 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/nodes/api/__init__.py b/nodes/api/__init__.py index 85c565e9..e1a12446 100644 --- a/nodes/api/__init__.py +++ b/nodes/api/__init__.py @@ -8,6 +8,7 @@ import aiohttp from ..server_manager import LocalComfyStreamServer from .. import settings_storage +import subprocess routes = None server_manager = None @@ -215,3 +216,7 @@ async def manage_configuration(request): logging.error(f"Error managing configuration: {str(e)}") return web.json_response({"error": str(e)}, status=500) + @routes.post('/comfyui/restart') + async def manage_configuration(request): + subprocess.run(["supervisorctl", "restart", "comfyui"]) + return web.json_response({"success": True}, status=200) diff --git a/nodes/web/js/launcher.js b/nodes/web/js/launcher.js index 7caf6e4f..2ad697d2 100644 --- a/nodes/web/js/launcher.js +++ b/nodes/web/js/launcher.js @@ -98,6 +98,38 @@ document.addEventListener('comfy-extension-registered', (event) => { } }); +async function restartComfyUI() { + try { + const response = await fetch('/comfyui/restart', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: "" // No body needed + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("[ComfyStream] ComfyUI restart returned error response:", response.status, errorText); + try { + const errorData = JSON.parse(errorText); + throw new Error(errorData.error || `Server error: ${response.status}`); + } catch (e) { + throw new Error(`Server error: ${response.status} - ${errorText}`); + } + } + + const data = await response.json(); + + return data; + } catch (error) { + console.error('[ComfyStream] Error restarting ComfyUI:', error); + app.ui.dialog.show('Error', error.message || 'Failed to restart ComfyUI'); + throw error; + } +} + async function controlServer(action) { try { // Get settings from the settings manager @@ -256,6 +288,12 @@ const extension = { icon: "pi pi-cog", label: "Server Settings", function: openSettings + }, + { + id: "ComfyStream.RestartComfyUI", + icon: "pi pi-refresh", + label: "Restart ComfyUI", + function: restartComfyUI } ], @@ -270,7 +308,9 @@ const extension = { "ComfyStream.StopServer", "ComfyStream.RestartServer", null, // Separator - "ComfyStream.Settings" + "ComfyStream.Settings", + null, // Separator + "ComfyStream.RestartComfyUI" ] } ], @@ -300,6 +340,8 @@ const extension = { comfyStreamMenu.addItem("Restart Server", () => controlServer('restart'), { icon: "pi pi-refresh" }); comfyStreamMenu.addSeparator(); comfyStreamMenu.addItem("Server Settings", openSettings, { icon: "pi pi-cog" }); + comfyStreamMenu.addSeparator(); + comfyStreamMenu.addItem("Restart Server", () => restartComfyUI(), { icon: "pi pi-refresh" }); } // New menu system is handled automatically by the menuCommands registration From 0ed2303c76bfd2bcc5b3840bc94c6c6fad64ecb7 Mon Sep 17 00:00:00 2001 From: Brad P Date: Mon, 7 Apr 2025 16:44:44 -0500 Subject: [PATCH 20/24] add mgmt api UI to ComfyStream menu in ComfyUI, some fixes/improvements --- nodes/api/__init__.py | 37 ++ nodes/web/js/launcher.js | 2 +- nodes/web/js/settings.js | 1126 ++++++++++++++++++++++++++++++++++- server/api/api.py | 55 +- server/api/models/models.py | 70 ++- server/api/nodes/nodes.py | 82 ++- 6 files changed, 1257 insertions(+), 115 deletions(-) diff --git a/nodes/api/__init__.py b/nodes/api/__init__.py index e1a12446..f5f5532a 100644 --- a/nodes/api/__init__.py +++ b/nodes/api/__init__.py @@ -216,7 +216,44 @@ async def manage_configuration(request): logging.error(f"Error managing configuration: {str(e)}") return web.json_response({"error": str(e)}, status=500) + @routes.post('/comfystream/settings/manage') + async def manage_comfystream(request): + """Manage ComfyStream server settings""" + #check if server is running + server_status = server_manager.get_status() + if not server_status["running"]: + return web.json_response({"error": "ComfyStream Server is not running"}, status=503) + + try: + data = await request.json() + action_type = data.get("action_type") + action = data.get("action") + payload = data.get("payload") + url_host = server_status.get("host", "localhost") + url_port = server_status.get("port", "8889") + mgmt_url = f"http://{url_host}:{url_port}/settings/{action_type}/{action}" + + async with aiohttp.ClientSession() as session: + async with session.post( + mgmt_url, + json=payload, + headers={"Content-Type": "application/json"} + ) as response: + if not response.ok: + return web.json_response( + {"error": f"Server error: {response.status}"}, + status=response.status + ) + return web.json_response(await response.json()) + except Exception as e: + logging.error(f"Error managing ComfyStream: {str(e)}") + return web.json_response({"error": str(e)}, status=500) + @routes.post('/comfyui/restart') async def manage_configuration(request): + server_status = server_manager.get_status() + if server_status["running"]: + await server_manager.stop() + subprocess.run(["supervisorctl", "restart", "comfyui"]) return web.json_response({"success": True}, status=200) diff --git a/nodes/web/js/launcher.js b/nodes/web/js/launcher.js index 2ad697d2..a2a5e58a 100644 --- a/nodes/web/js/launcher.js +++ b/nodes/web/js/launcher.js @@ -341,7 +341,7 @@ const extension = { comfyStreamMenu.addSeparator(); comfyStreamMenu.addItem("Server Settings", openSettings, { icon: "pi pi-cog" }); comfyStreamMenu.addSeparator(); - comfyStreamMenu.addItem("Restart Server", () => restartComfyUI(), { icon: "pi pi-refresh" }); + comfyStreamMenu.addItem("Restart ComfyUI", () => restartComfyUI(), { icon: "pi pi-refresh" }); } // New menu system is handled automatically by the menuCommands registration diff --git a/nodes/web/js/settings.js b/nodes/web/js/settings.js index 888917e8..90fcfebe 100644 --- a/nodes/web/js/settings.js +++ b/nodes/web/js/settings.js @@ -1,5 +1,6 @@ // ComfyStream Settings Manager console.log("[ComfyStream Settings] Initializing settings module"); +const app = window.comfyAPI?.app?.app; const DEFAULT_SETTINGS = { host: "0.0.0.0", @@ -12,6 +13,7 @@ class ComfyStreamSettings { constructor() { this.settings = DEFAULT_SETTINGS; this.loadSettings(); + } async loadSettings() { @@ -235,6 +237,32 @@ class ComfyStreamSettings { } return null; } + + async manageComfystream(action_type, action, data) { + try { + const response = await fetch('/comfystream/settings/manage', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + action_type: action_type, + action: action, + payload: data + }) + }); + + const result = await response.json(); + + if (response.ok) { + return result; + } else { + throw new Error(`${result.error}`); + } + } catch (error) { + throw error; + } + } } // Create a single instance of the settings manager @@ -257,7 +285,7 @@ async function showSettingsModal() { const style = document.createElement("style"); style.id = styleId; style.textContent = ` - #comfystream-settings-modal { + .comfystream-settings-modal { position: fixed; z-index: 10000; left: 0; @@ -506,6 +534,73 @@ async function showSettingsModal() { 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } + + .cs-help-text { + display: none; + width: 400px; + padding-left: 80px; + padding-bottom: 10px; + padding-top: 0px; + margin-top: 0px; + font-size: 0.75em; + overflow-wrap: break-word; + font-style: italic; + } + + .loader { + width: 20px; + height: 20px; + border-radius: 50%; + display: inline-block; + position: relative; + border: 2px solid; + border-color: #FFF #FFF transparent transparent; + box-sizing: border-box; + animation: rotation 1s linear infinite; + } + .loader::after, + .loader::before { + content: ''; + box-sizing: border-box; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; + border: 2px solid; + border-color: transparent transparent #FF3D00 #FF3D00; + width: 16px; + height: 16px; + border-radius: 50%; + box-sizing: border-box; + animation: rotationBack 0.5s linear infinite; + transform-origin: center center; + } + .loader::before { + width: 13px; + height: 13px; + border-color: #FFF #FFF transparent transparent; + animation: rotation 1.5s linear infinite; + } + + @keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + @keyframes rotationBack { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-360deg); + } + } + `; document.head.appendChild(style); } @@ -513,6 +608,7 @@ async function showSettingsModal() { // Create modal container const modal = document.createElement("div"); modal.id = "comfystream-settings-modal"; + modal.className = "comfystream-settings-modal"; // Create modal content const modalContent = document.createElement("div"); @@ -586,6 +682,69 @@ async function showSettingsModal() { portGroup.appendChild(portLabel); portGroup.appendChild(portInput); + // Comfystream mgmt api actions + // Nodes management group + const nodesGroup = document.createElement("div"); + nodesGroup.className = "cs-input-group"; + + const nodesLabel = document.createElement("label"); + nodesLabel.textContent = "Nodes:"; + nodesLabel.className = "cs-label"; + + const installNodeButton = document.createElement("button"); + installNodeButton.textContent = "Install"; + installNodeButton.className = "cs-button"; + + const updateNodeButton = document.createElement("button"); + updateNodeButton.textContent = "Update"; + updateNodeButton.className = "cs-button"; + + const deleteNodeButton = document.createElement("button"); + deleteNodeButton.textContent = "Delete"; + deleteNodeButton.className = "cs-button"; + + const toggleNodeButton = document.createElement("button"); + toggleNodeButton.textContent = "Enable/Disable"; + toggleNodeButton.className = "cs-button"; + + const loadingNodes = document.createElement("span"); + loadingNodes.id = "comfystream-loading-nodes-spinner"; + loadingNodes.className = "loader"; + loadingNodes.style.display = "none"; // Initially hidden + + nodesGroup.appendChild(nodesLabel); + nodesGroup.appendChild(installNodeButton); + nodesGroup.appendChild(updateNodeButton); + nodesGroup.appendChild(deleteNodeButton); + nodesGroup.appendChild(toggleNodeButton); + nodesGroup.appendChild(loadingNodes); + + // Models management group + const modelsGroup = document.createElement("div"); + modelsGroup.className = "cs-input-group"; + + const modelsLabel = document.createElement("label"); + modelsLabel.textContent = "Models:"; + modelsLabel.className = "cs-label"; + + const addModelButton = document.createElement("button"); + addModelButton.textContent = "Add"; + addModelButton.className = "cs-button"; + + const deleteModelButton = document.createElement("button"); + deleteModelButton.textContent = "Delete"; + deleteModelButton.className = "cs-button"; + + const loadingModels = document.createElement("span"); + loadingModels.id = "comfystream-loading-models-spinner"; + loadingModels.className = "loader"; + loadingModels.style.display = "none"; // Initially hidden + + modelsGroup.appendChild(modelsLabel); + modelsGroup.appendChild(addModelButton); + modelsGroup.appendChild(deleteModelButton); + modelsGroup.appendChild(loadingModels); + // Configurations section const configsSection = document.createElement("div"); configsSection.className = "cs-section"; @@ -659,6 +818,10 @@ async function showSettingsModal() { modal.remove(); }; + const msgTxt = document.createElement("div"); + msgTxt.id = "comfystream-msg-txt"; + msgTxt.className = "cs-msg-text"; + footer.appendChild(cancelButton); footer.appendChild(saveButton); @@ -666,18 +829,82 @@ async function showSettingsModal() { form.appendChild(currentConfigDiv); form.appendChild(hostGroup); form.appendChild(portGroup); + form.appendChild(nodesGroup); + form.appendChild(modelsGroup); form.appendChild(configsSection); modalContent.appendChild(closeButton); modalContent.appendChild(title); modalContent.appendChild(form); modalContent.appendChild(footer); + modalContent.appendChild(msgTxt); modal.appendChild(modalContent); // Add to document document.body.appendChild(modal); + async function manageNodes(action) { + //show the spinner to provide feedback + const loadingSpinner = document.getElementById("comfystream-loading-nodes-spinner"); + loadingSpinner.style.display = "inline-block"; + + try { + if (action === "install") { + await showInstallNodesModal(); + } else if (action === "update") { + await showUpdateNodesModal(); + } else if (action === "delete") { + await showDeleteNodesModal(); + } else if (action === "toggle") { + await showToggleNodesModal(); + } + + // Hide the spinner after action + loadingSpinner.style.display = "none"; + } catch (error) { + console.error("[ComfyStream] Error installing node:", error); + app.ui.dialog.show('Error', `Failed to install node: ${error.message}`); + } + } + async function manageModels(action) { + //show the spinner to provide feedback + const loadingSpinner = document.getElementById("comfystream-loading-models-spinner"); + loadingSpinner.style.display = "inline-block"; + + try { + if (action === "add") { + await showAddModelsModal(); + } else if (action === "delete") { + await showDeleteModelsModal(); + } + // Hide the spinner after action + loadingSpinner.style.display = "none"; + } catch (error) { + console.error("[ComfyStream] Error managing models:", error); + app.ui.dialog.show('Error', `Failed to manage models: ${error.message}`); + } + } + // Add event listeners for nodes management buttons + installNodeButton.addEventListener("click", () => { + manageNodes("install"); + }); + updateNodeButton.addEventListener("click", () => { + manageNodes("update"); + }); + deleteNodeButton.addEventListener("click", () => { + manageNodes("delete"); + }); + toggleNodeButton.addEventListener("click", () => { + manageNodes("toggle"); + }); + // Add event listeners for models management buttons + addModelButton.addEventListener("click", () => { + manageModels("add"); + }); + deleteModelButton.addEventListener("click", () => { + manageModels("delete"); + }); // Update configurations list async function updateConfigsList() { configsList.innerHTML = ""; @@ -850,11 +1077,904 @@ async function showSettingsModal() { hostInput.focus(); } +async function showAddModelsModal() { + // Check if modal already exists and remove it + const existingModal = document.getElementById("comfystream-settings-add-model-modal"); + if (existingModal) { + existingModal.remove(); + } + + // Create nodes mgmt modal container + const modal = document.createElement("div"); + modal.id = "comfystream-settings-add-model-modal"; + modal.className = "comfystream-settings-modal"; + + // Create close button + const closeButton = document.createElement("button"); + closeButton.textContent = "×"; + closeButton.className = "cs-close-button"; + closeButton.onclick = () => { + modal.remove(); + }; + + // Create modal content + const modalContent = document.createElement("div"); + modalContent.className = "cs-modal-content"; + + // Create title + const title = document.createElement("h3"); + title.textContent = "Add Model"; + title.className = "cs-title"; + + // Create settings form + const form = document.createElement("div"); + + // URL of node to add + const modelUrlGroup = document.createElement("div"); + modelUrlGroup.className = "cs-input-group"; + + const modelUrlLabel = document.createElement("label"); + modelUrlLabel.textContent = "Url:"; + modelUrlLabel.className = "cs-label"; + + const modelUrlInput = document.createElement("input"); + modelUrlInput.id = "add-model-url"; + modelUrlInput.className = "cs-input"; + + + const modelUrlHelpIcon = document.createElement("span"); + modelUrlHelpIcon.innerHTML = "🛈"; // Unicode for a help icon (ℹ️) + modelUrlHelpIcon.style.cursor = "pointer"; + modelUrlHelpIcon.style.marginLeft = "5px"; + modelUrlHelpIcon.title = "Click for help"; + + const modelUrlHelp = document.createElement("div"); + modelUrlHelp.textContent = "Specify the url of the model download url"; + modelUrlHelp.className = "cs-help-text"; + modelUrlHelp.style.display = "none"; + + modelUrlHelpIcon.addEventListener("click", () => { + if (modelUrlHelp.style.display == "none") { + modelUrlHelp.style.display = "block"; + } else { + modelUrlHelp.style.display = "none"; + } + }); + + modelUrlGroup.appendChild(modelUrlLabel); + modelUrlGroup.appendChild(modelUrlInput); + modelUrlGroup.appendChild(modelUrlHelpIcon); + + // branch of node to add + const modelTypeGroup = document.createElement("div"); + modelTypeGroup.className = "cs-input-group"; + + const modelTypeLabel = document.createElement("label"); + modelTypeLabel.textContent = "Type:"; + modelTypeLabel.className = "cs-label"; + + const modelTypeInput = document.createElement("input"); + modelTypeInput.id = "add-node-branch"; + modelTypeInput.className = "cs-input"; + + const modelTypeHelpIcon = document.createElement("span"); + modelTypeHelpIcon.innerHTML = "🛈"; // Unicode for a help icon (ℹ️) + modelTypeHelpIcon.style.cursor = "pointer"; + modelTypeHelpIcon.style.marginLeft = "5px"; + modelTypeHelpIcon.title = "Click for help"; + modelTypeHelpIcon.style.position = "relative"; + + const modelTypeHelp = document.createElement("div"); + modelTypeHelp.textContent = "Specify the type of model that is the top level folder under 'models' folder (e.g. 'checkpoints' = models/checkpoints)"; + modelTypeHelp.className = "cs-help-text"; + modelTypeHelp.style.display = "none"; + + modelTypeHelpIcon.addEventListener("click", () => { + if (modelTypeHelp.style.display == "none") { + modelTypeHelp.style.display = "block"; + } else { + modelTypeHelp.style.display = "none"; + } + }); + + modelTypeGroup.appendChild(modelTypeLabel); + modelTypeGroup.appendChild(modelTypeInput); + modelTypeGroup.appendChild(modelTypeHelpIcon); + + + // dependencies of node to add + const modelPathGroup = document.createElement("div"); + modelPathGroup.className = "cs-input-group"; + + const modelPathLabel = document.createElement("label"); + modelPathLabel.textContent = "Path:"; + modelPathLabel.className = "cs-label"; + + const modelPathInput = document.createElement("input"); + modelPathInput.id = "add-model-path"; + modelPathInput.className = "cs-input"; + + const modelPathHelpIcon = document.createElement("span"); + modelPathHelpIcon.innerHTML = "🛈"; // Unicode for a help icon (ℹ️) + modelPathHelpIcon.style.cursor = "pointer"; + modelPathHelpIcon.style.marginLeft = "5px"; + modelPathHelpIcon.title = "Click for help"; + + const modelPathHelp = document.createElement("div"); + modelPathHelp.textContent = "Input the path of the model file (including file name, 'SD1.5/model.safetensors' = checkpoints/SD1.5/model.safetensors)"; + modelPathHelp.className = "cs-help-text"; + modelPathHelp.style.display = "none"; + + modelPathHelpIcon.addEventListener("click", () => { + if (modelPathHelp.style.display == "none") { + modelPathHelp.style.display = "block"; + } else { + modelPathHelp.style.display = "none"; + } + }); + + modelPathGroup.appendChild(modelPathLabel); + modelPathGroup.appendChild(modelPathInput); + modelPathGroup.appendChild(modelPathHelpIcon); + + // Footer with buttons + const footer = document.createElement("div"); + footer.className = "cs-footer"; + + const msgTxt = document.createElement("div"); + msgTxt.id = "comfystream-manage-models-add-msg-txt"; + msgTxt.style.fontSize = "0.75em"; + msgTxt.style.fontStyle = "italic"; + msgTxt.style.overflowWrap = "break-word"; + + const cancelButton = document.createElement("button"); + cancelButton.textContent = "Cancel"; + cancelButton.className = "cs-button"; + cancelButton.onclick = () => { + modal.remove(); + }; + + const addButton = document.createElement("button"); + addButton.textContent = "Add"; + addButton.className = "cs-button primary"; + addButton.onclick = async () => { + const modelUrl = modelUrlInput.value; + const modelType = modelTypeInput.value; + const modelPath = modelPathInput.value; + const payload = { + url: modelUrl, + type: modelType, + path: modelPath + }; + + try { + await settingsManager.manageComfystream( + "models", + "add", + [payload] + ); + msgTxt.textContent = "Model added successfully!"; + } catch (error) { + console.error("[ComfyStream] Error adding model:", error); + msgTxt.textContent = error; + } + }; + + footer.appendChild(msgTxt); + footer.appendChild(cancelButton); + footer.appendChild(addButton); + + // Assemble the modal + form.appendChild(modelUrlGroup); + form.appendChild(modelUrlHelp); + form.appendChild(modelTypeGroup); + form.appendChild(modelTypeHelp); + form.appendChild(modelPathGroup); + form.appendChild(modelPathHelp); + + modalContent.appendChild(closeButton); + modalContent.appendChild(title); + modalContent.appendChild(form); + modalContent.appendChild(footer); + + modal.appendChild(modalContent); + + // Add to document + document.body.appendChild(modal); +} + +async function showDeleteModelsModal() { + // Check if modal already exists and remove it + const existingModal = document.getElementById("comfystream-settings-delete-model-modal"); + if (existingModal) { + existingModal.remove(); + } + + // Create nodes mgmt modal container + const modal = document.createElement("div"); + modal.id = "comfystream-settings-delete-model-modal"; + modal.className = "comfystream-settings-modal"; + + // Create close button + const closeButton = document.createElement("button"); + closeButton.textContent = "×"; + closeButton.className = "cs-close-button"; + closeButton.onclick = () => { + modal.remove(); + }; + + // Create modal content + const modalContent = document.createElement("div"); + modalContent.className = "cs-modal-content"; + + // Create title + const title = document.createElement("h3"); + title.textContent = "Delete Model"; + title.className = "cs-title"; + + // Create settings form + const form = document.createElement("div"); + + const msgTxt = document.createElement("div"); + msgTxt.id = "comfystream-manage-models-delete-msg-txt"; + msgTxt.style.fontSize = "0.75em"; + msgTxt.style.fontStyle = "italic"; + msgTxt.style.overflowWrap = "break-word"; + + //Get the nodes + const modelSelect = document.createElement("select"); + modelSelect.id = "comfystream-selected-model"; + try { + const models = await settingsManager.manageComfystream( + "models", + "list", + "" + ); + for (const model_type in models.models) { + for (const model of models.models[model_type]) { + const modelItem = document.createElement("option"); + modelItem.setAttribute("model-type", model.type); + modelItem.value = model.path; + modelItem.textContent = model.type + " | " + model.path; + modelSelect.appendChild(modelItem); + } + } + } catch (error) { + msgTxt.textContent = error; + } + + // Footer with buttons + const footer = document.createElement("div"); + footer.className = "cs-footer"; + + + const cancelButton = document.createElement("button"); + cancelButton.textContent = "Cancel"; + cancelButton.className = "cs-button"; + cancelButton.onclick = () => { + modal.remove(); + }; + + const deleteButton = document.createElement("button"); + deleteButton.textContent = "Delete"; + deleteButton.className = "cs-button primary"; + deleteButton.onclick = async () => { + const modelPath = modelSelect.options[modelSelect.selectedIndex].value; + const modelType = modelSelect.options[modelSelect.selectedIndex].getAttribute("model-type"); + const payload = { + type: modelType, + path: modelPath + }; + + try { + await settingsManager.manageComfystream( + "models", + "delete", + [payload] + ); + msgTxt.textContent = "Model deleted successfully"; + } catch (error) { + console.error("[ComfyStream] Error deleting model:", error); + msgTxt.textContent = error; + } + + + }; + + footer.appendChild(msgTxt); + footer.appendChild(cancelButton); + footer.appendChild(deleteButton); + + // Assemble the modal + form.appendChild(modelSelect); + + modalContent.appendChild(closeButton); + modalContent.appendChild(title); + modalContent.appendChild(form); + modalContent.appendChild(footer); + + modal.appendChild(modalContent); + + // Add to document + document.body.appendChild(modal); +} + +async function showToggleNodesModal() { + // Check if modal already exists and remove it + const existingModal = document.getElementById("comfystream-settings-toggle-nodes-modal"); + if (existingModal) { + existingModal.remove(); + } + + // Create nodes mgmt modal container + const modal = document.createElement("div"); + modal.id = "comfystream-settings-toggle-nodes-modal"; + modal.className = "comfystream-settings-modal"; + + // Create close button + const closeButton = document.createElement("button"); + closeButton.textContent = "×"; + closeButton.className = "cs-close-button"; + closeButton.onclick = () => { + modal.remove(); + }; + + // Create modal content + const modalContent = document.createElement("div"); + modalContent.className = "cs-modal-content"; + + // Create title + const title = document.createElement("h3"); + title.textContent = "Enable/Disable Custom Nodes"; + title.className = "cs-title"; + + // Create settings form + const form = document.createElement("div"); + const toggleNodesModalContent = document.createElement("div"); + toggleNodesModalContent.className = "cs-modal-content"; + + const msgTxt = document.createElement("div"); + msgTxt.id = "comfystream-manage-nodes-toggle-msg-txt"; + msgTxt.style.fontSize = "0.75em"; + msgTxt.style.fontStyle = "italic"; + msgTxt.style.overflowWrap = "break-word"; + + //Get the nodes + const nodeSelect = document.createElement("select"); + let initialAction = "Enable"; + nodeSelect.id = "comfystream-selected-node"; + try { + const nodes = await settingsManager.manageComfystream( + "nodes", + "list", + "" + ); + for (const node of nodes.nodes) { + const nodeItem = document.createElement("option"); + nodeItem.value = node.name; + nodeItem.textContent = node.name; + nodeItem.setAttribute("node-is-disabled", node.disabled); + if (!node.disabled) { + initialAction = "Disable"; + } + nodeSelect.appendChild(nodeItem); + } + } catch (error) { + msgTxt.textContent = error; + } + + // Footer with buttons + const footer = document.createElement("div"); + footer.className = "cs-footer"; + + + const cancelButton = document.createElement("button"); + cancelButton.textContent = "Cancel"; + cancelButton.className = "cs-button"; + cancelButton.onclick = () => { + modal.remove(); + }; + + const toggleButton = document.createElement("button"); + toggleButton.textContent = initialAction; + toggleButton.className = "cs-button primary"; + toggleButton.onclick = async () => { + const nodeName = nodeSelect.options[nodeSelect.selectedIndex].value; + const payload = { + name: nodeName, + }; + const action = toggleButton.textContent === "Enable" ? "enable" : "disable"; + + try { + await settingsManager.manageComfystream( + "nodes", + "toggle", + [payload] + ); + msgTxt.textContent = `Node ${action} successfully`; + } catch (error) { + + } + + + }; + + //update the action based on if the node is disabled or not currently + nodeSelect.onchange = () => { + const selectedNode = nodeSelect.options[nodeSelect.selectedIndex]; + const isDisabled = selectedNode.getAttribute("node-is-disabled") === "true"; + if (isDisabled) { + toggleButton.textContent = "Enable"; + } else { + toggleButton.textContent = "Disable"; + } + }; + + footer.appendChild(msgTxt); + footer.appendChild(cancelButton); + footer.appendChild(toggleButton); + + // Assemble the modal + form.appendChild(nodeSelect); + + modalContent.appendChild(closeButton); + modalContent.appendChild(title); + modalContent.appendChild(form); + modalContent.appendChild(footer); + + modal.appendChild(modalContent); + + // Add to document + document.body.appendChild(modal); +} + +async function showDeleteNodesModal() { + // Check if modal already exists and remove it + const existingModal = document.getElementById("comfystream-settings-delete-nodes-modal"); + if (existingModal) { + existingModal.remove(); + } + + // Create nodes mgmt modal container + const modal = document.createElement("div"); + modal.id = "comfystream-settings-delete-nodes-modal"; + modal.className = "comfystream-settings-modal"; + + // Create close button + const closeButton = document.createElement("button"); + closeButton.textContent = "×"; + closeButton.className = "cs-close-button"; + closeButton.onclick = () => { + modal.remove(); + }; + + // Create modal content + const modalContent = document.createElement("div"); + modalContent.className = "cs-modal-content"; + + // Create title + const title = document.createElement("h3"); + title.textContent = "Delete Custom Nodes"; + title.className = "cs-title"; + + // Create settings form + const form = document.createElement("div"); + + const msgTxt = document.createElement("div"); + msgTxt.id = "comfystream-manage-nodes-delete-msg-txt"; + msgTxt.style.fontSize = "0.75em"; + msgTxt.style.fontStyle = "italic"; + msgTxt.style.overflowWrap = "break-word"; + + //Get the nodes + const nodeSelect = document.createElement("select"); + nodeSelect.id = "comfystream-selected-node"; + try { + const nodes = await settingsManager.manageComfystream( + "nodes", + "list", + "" + ); + for (const node of nodes.nodes) { + const nodeItem = document.createElement("option"); + nodeItem.value = node.name; + nodeItem.textContent = node.name; + nodeSelect.appendChild(nodeItem); + } + } catch (error) { + msgTxt.textContent = error; + } + + // Footer with buttons + const footer = document.createElement("div"); + footer.className = "cs-footer"; + + + const cancelButton = document.createElement("button"); + cancelButton.textContent = "Cancel"; + cancelButton.className = "cs-button"; + cancelButton.onclick = () => { + modal.remove(); + }; + + const deleteButton = document.createElement("button"); + deleteButton.textContent = "Delete"; + deleteButton.className = "cs-button primary"; + deleteButton.onclick = async () => { + const nodeName = nodeSelect.options[nodeSelect.selectedIndex].value; + const payload = { + name: nodeName, + }; + + try { + await settingsManager.manageComfystream( + "nodes", + "delete", + [payload] + ); + msgTxt.textContent = "Node deleted successfully"; + } catch (error) { + console.error("[ComfyStream] Error deleting node:", error); + msgTxt.textContent = error; + } + + + }; + + footer.appendChild(msgTxt); + footer.appendChild(cancelButton); + footer.appendChild(deleteButton); + + // Assemble the modal + form.appendChild(nodeSelect); + + modalContent.appendChild(closeButton); + modalContent.appendChild(title); + modalContent.appendChild(form); + modalContent.appendChild(footer); + + modal.appendChild(modalContent); + + // Add to document + document.body.appendChild(modal); +} + +async function showUpdateNodesModal() { + // Check if modal already exists and remove it + const existingModal = document.getElementById("comfystream-settings-update-nodes-modal"); + if (existingModal) { + existingModal.remove(); + } + + // Create nodes mgmt modal container + const modal = document.createElement("div"); + modal.id = "comfystream-settings-update-nodes-modal"; + modal.className = "comfystream-settings-modal"; + + // Create close button + const closeButton = document.createElement("button"); + closeButton.textContent = "×"; + closeButton.className = "cs-close-button"; + closeButton.onclick = () => { + modal.remove(); + }; + + // Create modal content + const modalContent = document.createElement("div"); + modalContent.className = "cs-modal-content"; + + // Create title + const title = document.createElement("h3"); + title.textContent = "Update Custom Nodes"; + title.className = "cs-title"; + + // Create settings form + const form = document.createElement("div"); + + const msgTxt = document.createElement("div"); + msgTxt.id = "comfystream-manage-nodes-update-msg-txt"; + msgTxt.style.fontSize = "0.75em"; + msgTxt.style.fontStyle = "italic"; + msgTxt.style.overflowWrap = "break-word"; + + //Get the nodes + const nodeSelect = document.createElement("select"); + nodeSelect.id = "comfystream-selected-node"; + try { + const nodes = await settingsManager.manageComfystream( + "nodes", + "list", + "" + ); + let updateAvailable = false; + for (const node of nodes.nodes) { + if (node.update_available && node.update_available != "unknown" && node.url != "unknown") { + updateAvailable = true; + const nodeItem = document.createElement("option"); + nodeItem.value = node.url; + nodeItem.textContent = node.name; + nodeSelect.appendChild(nodeItem); + } + } + if (!updateAvailable) { + msgTxt.textContent = "No updates available for any nodes."; + } + } catch (error) { + msgTxt.textContent = error; + } + + // Footer with buttons + const footer = document.createElement("div"); + footer.className = "cs-footer"; + + + const cancelButton = document.createElement("button"); + cancelButton.textContent = "Cancel"; + cancelButton.className = "cs-button"; + cancelButton.onclick = () => { + modal.remove(); + }; + + const installButton = document.createElement("button"); + installButton.textContent = "Update"; + installButton.className = "cs-button primary"; + installButton.onclick = async () => { + const nodeUrl = nodeSelect.options[nodeSelect.selectedIndex].value; + const payload = { + url: nodeUrl, + }; + + try { + await settingsManager.manageComfystream( + "nodes", + "install", + [payload] + ); + msgTxt.textContent = "Node updated successfully"; + } catch (error) { + console.error("[ComfyStream] Error updating node:", error); + msgTxt.textContent = error; + } + + + }; + + footer.appendChild(msgTxt); + footer.appendChild(cancelButton); + footer.appendChild(installButton); + + // Assemble the modal + form.appendChild(nodeSelect); + + modalContent.appendChild(closeButton); + modalContent.appendChild(title); + modalContent.appendChild(form); + modalContent.appendChild(footer); + + modal.appendChild(modalContent); + + // Add to document + document.body.appendChild(modal); +} + +async function showInstallNodesModal() { + // Check if modal already exists and remove it + const existingModal = document.getElementById("comfystream-settings-add-nodes-modal"); + if (existingModal) { + existingModal.remove(); + } + + // Create nodes mgmt modal container + const modal = document.createElement("div"); + modal.id = "comfystream-settings-add-nodes-modal"; + modal.className = "comfystream-settings-modal"; + + // Create close button + const closeButton = document.createElement("button"); + closeButton.textContent = "×"; + closeButton.className = "cs-close-button"; + closeButton.onclick = () => { + modal.remove(); + }; + + // Create modal content + const modalContent = document.createElement("div"); + modalContent.className = "cs-modal-content"; + + // Create title + const title = document.createElement("h3"); + title.textContent = "Add Custom Nodes"; + title.className = "cs-title"; + + // Create settings form + const form = document.createElement("div"); + + // URL of node to add + const nodeUrlGroup = document.createElement("div"); + nodeUrlGroup.className = "cs-input-group"; + + const nodeUrlLabel = document.createElement("label"); + nodeUrlLabel.textContent = "Url:"; + nodeUrlLabel.className = "cs-label"; + + const nodeUrlInput = document.createElement("input"); + nodeUrlInput.id = "add-node-url"; + nodeUrlInput.className = "cs-input"; + + + const nodeUrlHelpIcon = document.createElement("span"); + nodeUrlHelpIcon.innerHTML = "🛈"; // Unicode for a help icon (ℹ️) + nodeUrlHelpIcon.style.cursor = "pointer"; + nodeUrlHelpIcon.style.marginLeft = "5px"; + nodeUrlHelpIcon.title = "Click for help"; + + const nodeUrlHelp = document.createElement("div"); + nodeUrlHelp.textContent = "Specify the url of the github repo for the custom node want to install (can have .git at end of url)"; + nodeUrlHelp.className = "cs-help-text"; + nodeUrlHelp.style.display = "none"; + + nodeUrlHelpIcon.addEventListener("click", () => { + if (nodeUrlHelp.style.display == "none") { + nodeUrlHelp.style.display = "block"; + } else { + nodeUrlHelp.style.display = "none"; + } + }); + + nodeUrlGroup.appendChild(nodeUrlLabel); + nodeUrlGroup.appendChild(nodeUrlInput); + nodeUrlGroup.appendChild(nodeUrlHelpIcon); + + // branch of node to add + const nodeBranchGroup = document.createElement("div"); + nodeBranchGroup.className = "cs-input-group"; + + const nodeBranchLabel = document.createElement("label"); + nodeBranchLabel.textContent = "Branch:"; + nodeBranchLabel.className = "cs-label"; + + const nodeBranchInput = document.createElement("input"); + nodeBranchInput.id = "add-node-branch"; + nodeBranchInput.className = "cs-input"; + + const nodeBranchHelpIcon = document.createElement("span"); + nodeBranchHelpIcon.innerHTML = "🛈"; // Unicode for a help icon (ℹ️) + nodeBranchHelpIcon.style.cursor = "pointer"; + nodeBranchHelpIcon.style.marginLeft = "5px"; + nodeBranchHelpIcon.title = "Click for help"; + nodeBranchHelpIcon.style.position = "relative"; + + const nodeBranchHelp = document.createElement("div"); + nodeBranchHelp.textContent = "Specify the branch of the node you want to add. For example, 'main' or 'develop'."; + nodeBranchHelp.className = "cs-help-text"; + nodeBranchHelp.style.display = "none"; + + nodeBranchHelpIcon.addEventListener("click", () => { + if (nodeBranchHelp.style.display == "none") { + nodeBranchHelp.style.display = "block"; + } else { + nodeBranchHelp.style.display = "none"; + } + }); + + nodeBranchGroup.appendChild(nodeBranchLabel); + nodeBranchGroup.appendChild(nodeBranchInput); + nodeBranchGroup.appendChild(nodeBranchHelpIcon); + + + // dependencies of node to add + const nodeDepsGroup = document.createElement("div"); + nodeDepsGroup.className = "cs-input-group"; + + const nodeDepsLabel = document.createElement("label"); + nodeDepsLabel.textContent = "Deps:"; + nodeDepsLabel.className = "cs-label"; + + const nodeDepsInput = document.createElement("input"); + nodeDepsInput.id = "add-node-deps"; + nodeDepsInput.className = "cs-input"; + + const nodeDepsHelpIcon = document.createElement("span"); + nodeDepsHelpIcon.innerHTML = "🛈"; // Unicode for a help icon (ℹ️) + nodeDepsHelpIcon.style.cursor = "pointer"; + nodeDepsHelpIcon.style.marginLeft = "5px"; + nodeDepsHelpIcon.title = "Click for help"; + + const nodeDepsHelp = document.createElement("div"); + nodeDepsHelp.textContent = "Comma separated list of python packages to install with pip (required packages outside requirements.txt)"; + nodeDepsHelp.className = "cs-help-text"; + nodeDepsHelp.style.display = "none"; + + nodeDepsHelpIcon.addEventListener("click", () => { + if (nodeDepsHelp.style.display == "none") { + nodeDepsHelp.style.display = "block"; + } else { + nodeDepsHelp.style.display = "none"; + } + }); + + nodeDepsGroup.appendChild(nodeDepsLabel); + nodeDepsGroup.appendChild(nodeDepsInput); + nodeDepsGroup.appendChild(nodeDepsHelpIcon); + + // Footer with buttons + const footer = document.createElement("div"); + footer.className = "cs-footer"; + + const msgTxt = document.createElement("div"); + msgTxt.id = "comfystream-manage-nodes-install-msg-txt"; + msgTxt.style.fontSize = "0.75em"; + msgTxt.style.fontStyle = "italic"; + msgTxt.style.overflowWrap = "break-word"; + + const cancelButton = document.createElement("button"); + cancelButton.textContent = "Cancel"; + cancelButton.className = "cs-button"; + cancelButton.onclick = () => { + modal.remove(); + }; + + const installButton = document.createElement("button"); + installButton.textContent = "Install"; + installButton.className = "cs-button primary"; + installButton.onclick = async () => { + const nodeUrl = nodeUrlInput.value; + const nodeBranch = nodeBranchInput.value; + const nodeDeps = nodeDepsInput.value; + const payload = { + url: nodeUrl, + branch: nodeBranch, + dependencies: nodeDeps + }; + + try { + await settingsManager.manageComfystream( + "nodes", + "install", + [payload] + ); + msgTxt.textContent = "Node installed successfully!"; + } catch (error) { + console.error("[ComfyStream] Error installing node:", error); + msgTxt.textContent = error; + } + + + }; + + footer.appendChild(msgTxt); + footer.appendChild(cancelButton); + footer.appendChild(installButton); + + // Assemble the modal + form.appendChild(nodeUrlGroup); + form.appendChild(nodeUrlHelp); + form.appendChild(nodeBranchGroup); + form.appendChild(nodeBranchHelp); + form.appendChild(nodeDepsGroup); + form.appendChild(nodeDepsHelp); + + modalContent.appendChild(closeButton); + modalContent.appendChild(title); + modalContent.appendChild(form); + modalContent.appendChild(footer); + + modal.appendChild(modalContent); + + // Add to document + document.body.appendChild(modal); +} // Export for use in other modules -export { settingsManager, showSettingsModal }; +export { settingsManager, showSettingsModal, showInstallNodesModal, showUpdateNodesModal, showDeleteNodesModal, showToggleNodesModal, showAddModelsModal, showDeleteModelsModal }; // Also keep the global for backward compatibility window.comfyStreamSettings = { settingsManager, - showSettingsModal + showSettingsModal, + showInstallNodesModal, + showUpdateNodesModal, + showDeleteNodesModal, + showToggleNodesModal, + showAddModelsModal, + showDeleteModelsModal }; \ No newline at end of file diff --git a/server/api/api.py b/server/api/api.py index 1e6f1339..84243037 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -1,7 +1,7 @@ from aiohttp import web import asyncio -from api.nodes.nodes import list_nodes, install_node, delete_node, enable_node, disable_node +from api.nodes.nodes import list_nodes, install_node, delete_node, toggle_node from api.models.models import list_models, add_model, delete_model from api.settings.settings import set_twilio_account_info, restart_comfyui from pipeline import Pipeline @@ -14,12 +14,13 @@ def add_mgmt_api_routes(app): app.router.add_get("/settings/nodes/list", nodes) + app.router.add_post("/settings/nodes/list", nodes) app.router.add_post("/settings/nodes/install", install_nodes) app.router.add_post("/settings/nodes/delete", delete_nodes) - app.router.add_post("/settings/nodes/enable", enable_nodes) - app.router.add_post("/settings/nodes/disable", disable_nodes) + app.router.add_post("/settings/nodes/toggle", toggle_nodes) app.router.add_get("/settings/models/list", models) + app.router.add_post("/settings/models/list", models) app.router.add_post("/settings/models/add", add_models) app.router.add_post("/settings/models/delete", delete_models) @@ -83,9 +84,9 @@ async def nodes(request): workspace_dir = request.app["workspace"] try: nodes = await list_nodes(workspace_dir) - return web.json_response({"error": None, "nodes": nodes}) + return web.json_response({"success": True, "error": None, "nodes": nodes}) except Exception as e: - return web.json_response({"error": str(e), "nodes": nodes}, status=500) + return web.json_response({"success": False, "error": str(e), "nodes": nodes}, status=500) async def install_nodes(request): ''' @@ -151,9 +152,9 @@ async def delete_nodes(request): except Exception as e: return web.json_response({"success": False, "error": str(e), "deleted_nodes": deleted_nodes}, status=500) -async def enable_nodes(request): +async def toggle_nodes(request): ''' - Enable ComfyUI custom node + Enable/Disable ComfyUI custom node # Parameters: name: name of the node (e.g. ComfyUI-Custom-Node) @@ -171,41 +172,13 @@ async def enable_nodes(request): workspace_dir = request.app["workspace"] try: nodes = await request.json() - enabled_nodes = [] + toggled_nodes = [] for node in nodes: - await enable_node(node, workspace_dir) - enabled_nodes.append(node['name']) - return web.json_response({"success": True, "error": None, "enabled_nodes": enabled_nodes}) + await toggle_node(node, workspace_dir) + toggled_nodes.append(node['name']) + return web.json_response({"success": True, "error": None, "toggled_nodes": toggled_nodes}) except Exception as e: - return web.json_response({"success": False, "error": str(e), "enabled_nodes": enabled_nodes}, status=500) - -async def disable_nodes(request): - ''' - Disable ComfyUI custom node - - # Parameters: - name: name of the node (e.g. ComfyUI-Custom-Node) - - # Example request: - [ - { - "name": "ComfyUI-Custom-Node" - }, - { - ... - } - ] - ''' - workspace_dir = request.app["workspace"] - try: - nodes = await request.json() - disabled_nodes = [] - for node in nodes: - await disable_node(node, workspace_dir) - disabled_nodes.append(node['name']) - return web.json_response({"success": True, "error": None, "disabled_nodes": disabled_nodes}) - except Exception as e: - return web.json_response({"success": False, "error": str(e), "disabled_nodes": disabled_nodes}, status=500) + return web.json_response({"success": False, "error": str(e), "toggled_nodes": toggled_nodes}, status=500) async def models(request): ''' @@ -265,7 +238,7 @@ async def models(request): models = await list_models(workspace_dir) return web.json_response({"error": None, "models": models}) except Exception as e: - return web.json_response({"error": str(e), "models": models}, status=500) + return web.json_response({"error": str(e), "models": {}}, status=500) async def add_models(request): ''' diff --git a/server/api/models/models.py b/server/api/models/models.py index 2fa11158..e7d2e4a6 100644 --- a/server/api/models/models.py +++ b/server/api/models/models.py @@ -14,34 +14,55 @@ async def list_models(workspace_dir): model_types = ["checkpoints", "controlnet", "unet", "vae", "onnx", "tensorrt"] models = {} - for model_type in model_types: - models[model_type] = [] - model_path = models_path / model_type - if not model_path.exists(): - continue - for model in model_path.iterdir(): - if model.is_dir(): - for submodel in model.iterdir(): - if submodel.is_file(): - model_info = { - "name": submodel.name, - "path": f"{model.name}/{submodel.name}", - "type": model_type, - "downloading": os.path.exists(f"{model_type}/{model.name}/{submodel.name}.downloading") - } - models[model_type].append(model_info) + try: + for model_type in models_path.iterdir(): + model_name = "" + model_subfolder = "" + model_type_name = model_type.name + if model_type.is_dir(): + models[model_type_name] = [] + for model in model_type.iterdir(): + if model.is_dir(): + #models in subfolders (e.g. checkpoints/sd1.5/model.safetensors) + for submodel in model.iterdir(): + if submodel.is_file(): + model_name = submodel.name + model_subfolder = model.name + else: + #models not in subfolders (e.g. checkpoints/model.safetensors) + logger.info(f"model: {model.name}") + model_name = model.name + model_subfolder = "" + + #add model to list + model_info = await create_model_info(model_name, model_subfolder, model_type_name) + models[model_type_name].append(model_info) + else: + if not model_type.name in model_types: + models["none"] = [] - if model.is_file(): - model_info = { - "name": model.name, - "path": f"{model.name}", - "type": model_type, - "downloading": os.path.exists(f"{model_type}/{model.name}.downloading") - } - models[model_type].append(model_info) + model_name = model_type_name + model_subfolder = "" + #add model to list + model_info = await create_model_info(model_name, model_subfolder, model_type_name) + models[model_type_name].append(model_info) + except Exception as e: + logger.error(f"error listing models: {e}") + raise Exception(f"error listing models: {e}") return models +async def create_model_info(model, model_subfolder, model_type): + model_path = f"{model_subfolder}/{model}" if model_subfolder else model + logger.info(f"adding info for model: {model_type}/{model_path}") + model_info = { + "name": model, + "path": model_path, + "type": model_type, + "downloading": os.path.exists(f"{model_path}.downloading") + } + return model_info + async def add_model(model, workspace_dir): if not 'url' in model: raise Exception("model url is required") @@ -51,6 +72,7 @@ async def add_model(model, workspace_dir): try: model_name = model['url'].split("/")[-1] model_path = Path(os.path.join(workspace_dir, "models", model['type'], model_name)) + #if specified, use the model path from the model dict (e.g. sd1.5/model.safetensors will put model.safetensors in models/checkpoints/sd1.5) if 'path' in model: model_path = Path(os.path.join(workspace_dir, "models", model['type'], model['path'])) logger.info(f"model path: {model_path}") diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py index 39eaa772..44f409d3 100644 --- a/server/api/nodes/nodes.py +++ b/server/api/nodes/nodes.py @@ -27,7 +27,11 @@ async def list_nodes(workspace_dir): "branch": "unknown", "commit": "unknown", "update_available": "unknown", + "disabled": False, } + if node.name.endswith(".disabled"): + node_info["disabled"] = True + node_info["name"] = node.name[:-9] #include VERSION if set in file version_file = os.path.join(custom_nodes_path, node.name, "VERSION") @@ -82,14 +86,16 @@ async def install_node(node, workspace_dir): # Clone and install the repository if it doesn't already exist logger.info(f"installing {dir_name}...") repo = Repo.clone_from(node["url"], node_path, depth=1) - if "branch" in node: + if "branch" in node and node["branch"] != "": repo.git.checkout(node['branch']) else: # Update the repository if it already exists logger.info(f"updating node {dir_name}") repo = Repo(node_path) repo.remotes.origin.fetch() - branch = node.get("branch", repo.remotes.origin.refs[0].remote_head) + branch = node.get("branch", "") + if branch == "": + branch = repo.remotes.origin.refs[0].remote_head repo.remotes.origin.pull(branch) @@ -99,7 +105,7 @@ async def install_node(node, workspace_dir): subprocess.run(["conda", "run", "-n", "comfystream", "pip", "install", "-r", str(requirements_file)], check=True) # Install additional dependencies if specified - if "dependencies" in node: + if "dependencies" in node and node["dependencies"] != "": for dep in node["dependencies"].split(','): subprocess.run(["conda", "run", "-n", "comfystream", "pip", "install", dep.strip()], check=True) @@ -125,61 +131,47 @@ async def delete_node(node, workspace_dir): logger.error(f"error deleting node {node['name']}") raise Exception(f"error deleting node: {e}") -async def enable_node(node, workspace_dir): +async def toggle_node(node, workspace_dir): custom_nodes_path = Path(os.path.join(workspace_dir, "custom_nodes")) custom_nodes_path.mkdir(parents=True, exist_ok=True) os.chdir(custom_nodes_path) - if "name" not in node: + if "name" not in node: + logger.error("name is required") raise ValueError("name is required") node_path = custom_nodes_path / node["name"] - #check if enabled node exists + is_disabled = False + #confirm if enabled node exists + logger.info(f"toggling node { node['name'] }") if not node_path.exists(): #try with .disabled - node_path = custom_nodes_path / node["name"]+".disabled" + node_path = custom_nodes_path / str(node['name']+".disabled") + logger.info(f"checking if node { node['name'] } is disabled") if not node_path.exists(): #node does not exist as enabled or disabled - raise ValueError(f"node {node['name']} does not exist") + logger.info(f"node { node['name'] }.disabled does not exist") + raise ValueError(f"node { node['name'] } does not exist") else: - #enable the disabled node - try: - #rename the folder to remove .disabled - logger.info(f"enabling node {node['name']}") - node_path.rename(custom_nodes_path / node["name"]) - except Exception as e: - logger.error(f"error enabling node {node['name']}") - raise Exception(f"error enabling node: {e}") + #node is disabled, so we need to enable it + logger.error(f"node { node['name'] } is disabled") + is_disabled = True else: - #node is already enabled, nothing to do - return + logger.info(f"node { node['name'] } is enabled") -async def disable_node(node, workspace_dir): - custom_nodes_path = Path(os.path.join(workspace_dir, "custom_nodes")) - custom_nodes_path.mkdir(parents=True, exist_ok=True) - os.chdir(custom_nodes_path) - if "name" not in node: - raise ValueError("name is required") - - node_path = custom_nodes_path / node["name"] - if not node_path.exists(): - #try with .disabled - node_path = custom_nodes_path / node["name"]+".disabled" - if not node_path.exists(): - #node does not exist as enabled or disabled - raise ValueError(f"node {node['name']} does not exist") - else: - #node is already disabled, nothing to do - return - else: - #rename the folder to add .disabled - try: - #delete the folder and all its contents. ignore_errors allows readonly files to be deleted + try: + if is_disabled: + #rename the folder to remove .disabled logger.info(f"enabling node {node['name']}") - node_path.rename(custom_nodes_path / node["name"]+".disabled") - except Exception as e: - logger.error(f"error enabling node {node['name']}") - raise Exception(f"error enabling node: {e}") - + new_name = node_path.with_name(node["name"]) + shutil.move(str(node_path), str(new_name)) + else: + #rename the folder to add .disabled + logger.info(f"disbling node {node['name']}") + new_name = node_path.with_name(node["name"]+".disabled") + shutil.move(str(node_path), str(new_name)) + except Exception as e: + logger.error(f"error {action} node {node['name']}: {e}") + raise Exception(f"error {action} node: {e}") from comfy.nodes.package import ExportedNodes from comfy.nodes.package import _comfy_nodes, _import_and_enumerate_nodes_in_module @@ -194,8 +186,6 @@ def force_import_all_nodes_in_workspace(vanilla_custom_nodes=True, raise_on_fail from comfy.nodes import base_nodes from comfy.nodes.vanilla_node_importing import mitigated_import_of_vanilla_custom_nodes - # only load these nodes once - base_and_extra = reduce(lambda x, y: x.update(y), map(lambda module_inner: _import_and_enumerate_nodes_in_module(module_inner, raise_on_failure=raise_on_failure), [ # this is the list of default nodes to import From 3fc8196a63d55c921e06aedf1dd0cfeea1dd4c17 Mon Sep 17 00:00:00 2001 From: Brad P Date: Mon, 14 Apr 2025 11:51:49 -0500 Subject: [PATCH 21/24] add mgmt of TURN server credentials --- nodes/web/js/settings.js | 272 ++++++++++++++++++++++++++++++++++++++- server/api/api.py | 2 +- 2 files changed, 270 insertions(+), 4 deletions(-) diff --git a/nodes/web/js/settings.js b/nodes/web/js/settings.js index 90fcfebe..34ae539e 100644 --- a/nodes/web/js/settings.js +++ b/nodes/web/js/settings.js @@ -745,6 +745,27 @@ async function showSettingsModal() { modelsGroup.appendChild(deleteModelButton); modelsGroup.appendChild(loadingModels); + // turn server creds group + const turnServerCredsGroup = document.createElement("div"); + turnServerCredsGroup.className = "cs-input-group"; + + const turnServerCredsLabel = document.createElement("label"); + turnServerCredsLabel.textContent = "TURN Creds:"; + turnServerCredsLabel.className = "cs-label"; + + const setButton = document.createElement("button"); + setButton.textContent = "Set"; + setButton.className = "cs-button"; + + const turnServerCredsLoading = document.createElement("span"); + turnServerCredsLoading.id = "comfystream-loading-turn-server-creds-spinner"; + turnServerCredsLoading.className = "loader"; + turnServerCredsLoading.style.display = "none"; // Initially hidden + + turnServerCredsGroup.appendChild(turnServerCredsLabel); + turnServerCredsGroup.appendChild(setButton); + turnServerCredsGroup.appendChild(turnServerCredsLoading); + // Configurations section const configsSection = document.createElement("div"); configsSection.className = "cs-section"; @@ -831,6 +852,7 @@ async function showSettingsModal() { form.appendChild(portGroup); form.appendChild(nodesGroup); form.appendChild(modelsGroup); + form.appendChild(turnServerCredsGroup); form.appendChild(configsSection); modalContent.appendChild(closeButton); @@ -885,6 +907,27 @@ async function showSettingsModal() { app.ui.dialog.show('Error', `Failed to manage models: ${error.message}`); } } + async function manageTurnServerCredentials(action) { + //show the spinner to provide feedback + const loadingSpinner = document.getElementById("comfystream-loading-turn-server-creds-spinner"); + loadingSpinner.style.display = "inline-block"; + + try { + if (action === "set") { + await showSetTurnServerCredsModal(); + } else if (action === "clear") { + await showClearTurnServerCredsModal(); + } + // Hide the spinner after action + loadingSpinner.style.display = "none"; + } catch (error) { + console.error("[ComfyStream] Error managing TURN server credentials:", error); + app.ui.dialog.show('Error', `Failed to manage TURN server credentials: ${error.message}`); + } + + // Hide the spinner after action + loadingSpinner.style.display = "none"; + } // Add event listeners for nodes management buttons installNodeButton.addEventListener("click", () => { manageNodes("install"); @@ -905,6 +948,9 @@ async function showSettingsModal() { deleteModelButton.addEventListener("click", () => { manageModels("delete"); }); + setButton.addEventListener("click", async () => { + await showSetTurnServerCredsModal(); + }); // Update configurations list async function updateConfigsList() { configsList.innerHTML = ""; @@ -1077,6 +1123,225 @@ async function showSettingsModal() { hostInput.focus(); } +async function showSetTurnServerCredsModal() { + // Check if modal already exists and remove it + const existingModal = document.getElementById("comfystream-settings-set-turn-creds-modal"); + if (existingModal) { + existingModal.remove(); + } + + // Create nodes mgmt modal container + const modal = document.createElement("div"); + modal.id = "comfystream-settings-set-turn-creds-modal"; + modal.className = "comfystream-settings-modal"; + + // Create close button + const closeButton = document.createElement("button"); + closeButton.textContent = "×"; + closeButton.className = "cs-close-button"; + closeButton.onclick = () => { + modal.remove(); + }; + + // Create modal content + const modalContent = document.createElement("div"); + modalContent.className = "cs-modal-content"; + + // Create title + const title = document.createElement("h3"); + title.textContent = "Set TURN Server Credentials"; + title.className = "cs-title"; + + // Create settings form + const form = document.createElement("div"); + + // account type + const accountTypeGroup = document.createElement("div"); + accountTypeGroup.className = "cs-input-group"; + + const accountTypeLabel = document.createElement("label"); + accountTypeLabel.textContent = "Account Type:"; + accountTypeLabel.className = "cs-label"; + + const accountTypeSelect = document.createElement("select"); + const accountItem = document.createElement("option"); + accountItem.value = "twilio"; + accountItem.textContent = "Twilio"; + accountTypeSelect.appendChild(accountItem); + accountTypeSelect.id = "comfystream-selected-turn-server-account-type"; + + const accountTypeHelpIcon = document.createElement("span"); + accountTypeHelpIcon.innerHTML = "🛈"; // Unicode for a help icon (ℹ️) + accountTypeHelpIcon.style.cursor = "pointer"; + accountTypeHelpIcon.style.marginLeft = "5px"; + accountTypeHelpIcon.title = "Click for help"; + + const accountTypeHelp = document.createElement("div"); + accountTypeHelp.textContent = "Specify the account type to use"; + accountTypeHelp.className = "cs-help-text"; + accountTypeHelp.style.display = "none"; + + accountTypeHelpIcon.addEventListener("click", () => { + if (accountTypeHelp.style.display == "none") { + accountTypeHelp.style.display = "block"; + } else { + accountTypeHelp.style.display = "none"; + } + }); + + accountTypeGroup.appendChild(accountTypeLabel); + accountTypeGroup.appendChild(accountTypeSelect); + accountTypeGroup.appendChild(accountTypeHelpIcon); + + // account id + const accountIdGroup = document.createElement("div"); + accountIdGroup.className = "cs-input-group"; + + const accountIdLabel = document.createElement("label"); + accountIdLabel.textContent = "Account ID:"; + accountIdLabel.className = "cs-label"; + + const accountIdInput = document.createElement("input"); + accountIdInput.id = "turn-server-creds-account-id"; + accountIdInput.className = "cs-input"; + + + const accountIdHelpIcon = document.createElement("span"); + accountIdHelpIcon.innerHTML = "🛈"; // Unicode for a help icon (ℹ️) + accountIdHelpIcon.style.cursor = "pointer"; + accountIdHelpIcon.style.marginLeft = "5px"; + accountIdHelpIcon.title = "Click for help"; + + const accountIdHelp = document.createElement("div"); + accountIdHelp.textContent = "Specify the account id for Twilio TURN server credentials"; + accountIdHelp.className = "cs-help-text"; + accountIdHelp.style.display = "none"; + + accountIdHelpIcon.addEventListener("click", () => { + if (accountIdHelp.style.display == "none") { + accountIdHelp.style.display = "block"; + } else { + accountIdHelp.style.display = "none"; + } + }); + + accountIdGroup.appendChild(accountIdLabel); + accountIdGroup.appendChild(accountIdInput); + accountIdGroup.appendChild(accountIdHelpIcon); + + // auth token + const accountAuthTokenGroup = document.createElement("div"); + accountAuthTokenGroup.className = "cs-input-group"; + + const accountAuthTokenLabel = document.createElement("label"); + accountAuthTokenLabel.textContent = "Auth Token:"; + accountAuthTokenLabel.className = "cs-label"; + + const accountAuthTokenInput = document.createElement("input"); + accountAuthTokenInput.id = "turn-server-creds-auth-token"; + accountAuthTokenInput.className = "cs-input"; + + const accountAuthTokenHelpIcon = document.createElement("span"); + accountAuthTokenHelpIcon.innerHTML = "🛈"; // Unicode for a help icon (ℹ️) + accountAuthTokenHelpIcon.style.cursor = "pointer"; + accountAuthTokenHelpIcon.style.marginLeft = "5px"; + accountAuthTokenHelpIcon.title = "Click for help"; + accountAuthTokenHelpIcon.style.position = "relative"; + + const accountAuthTokenHelp = document.createElement("div"); + accountAuthTokenHelp.textContent = "Specify the auth token provided by Twilio for TURN server credentials"; + accountAuthTokenHelp.className = "cs-help-text"; + accountAuthTokenHelp.style.display = "none"; + + accountAuthTokenHelpIcon.addEventListener("click", () => { + if (accountAuthTokenHelp.style.display == "none") { + accountAuthTokenHelp.style.display = "block"; + } else { + accountAuthTokenHelp.style.display = "none"; + } + }); + + accountAuthTokenGroup.appendChild(accountAuthTokenLabel); + accountAuthTokenGroup.appendChild(accountAuthTokenInput); + accountAuthTokenGroup.appendChild(accountAuthTokenHelpIcon); + + // Footer with buttons + const footer = document.createElement("div"); + footer.className = "cs-footer"; + + const msgTxt = document.createElement("div"); + msgTxt.id = "comfystream-manage-turn-server-creds-msg-txt"; + msgTxt.style.fontSize = "0.75em"; + msgTxt.style.fontStyle = "italic"; + msgTxt.style.overflowWrap = "break-word"; + + const cancelButton = document.createElement("button"); + cancelButton.textContent = "Cancel"; + cancelButton.className = "cs-button"; + cancelButton.onclick = () => { + modal.remove(); + }; + const clearButton = document.createElement("button"); + clearButton.textContent = "Clear"; + clearButton.className = "cs-button"; + clearButton.onclick = () => { + const accountType = accountTypeSelect.options[accountTypeSelect.selectedIndex].value; + msgTxt.textContent = setTurnSeverCreds(accountType, "", ""); + }; + + const setButton = document.createElement("button"); + setButton.textContent = "Set"; + setButton.className = "cs-button primary"; + setButton.onclick = async () => { + const accountId = accountIdInput.value; + const authToken = accountAuthTokenInput.value; + const accountType = accountTypeSelect.options[accountTypeSelect.selectedIndex].value; + msgTxt.textContent = setTurnSeverCreds(accountType, accountId, authToken); + }; + + footer.appendChild(msgTxt); + footer.appendChild(cancelButton); + footer.appendChild(setButton); + + // Assemble the modal + form.appendChild(accountTypeGroup); + form.appendChild(accountTypeHelp); + form.appendChild(accountIdGroup); + form.appendChild(accountIdHelp); + form.appendChild(accountAuthTokenGroup); + form.appendChild(accountAuthTokenHelp); + + modalContent.appendChild(closeButton); + modalContent.appendChild(title); + modalContent.appendChild(form); + modalContent.appendChild(footer); + + modal.appendChild(modalContent); + + // Add to document + document.body.appendChild(modal); +} + +async function setTurnSeverCreds(accountType, accountId, authToken) { + try { + const payload = { + type: accountType, + account_id: accountId, + auth_token: authToken + } + + await settingsManager.manageComfystream( + "turn/server", + "set/account", + [payload] + ); + return "TURN server credentials updated successfully"; + } catch (error) { + console.error("[ComfyStream] Error adding model:", error); + msgTxt.textContent = error; + } +} + async function showAddModelsModal() { // Check if modal already exists and remove it const existingModal = document.getElementById("comfystream-settings-add-model-modal"); @@ -1253,7 +1518,7 @@ async function showAddModelsModal() { "add", [payload] ); - msgTxt.textContent = "Model added successfully!"; + msgTxt.textContent = "Model added successfully"; } catch (error) { console.error("[ComfyStream] Error adding model:", error); msgTxt.textContent = error; @@ -1965,7 +2230,7 @@ async function showInstallNodesModal() { document.body.appendChild(modal); } // Export for use in other modules -export { settingsManager, showSettingsModal, showInstallNodesModal, showUpdateNodesModal, showDeleteNodesModal, showToggleNodesModal, showAddModelsModal, showDeleteModelsModal }; +export { settingsManager, showSettingsModal, showInstallNodesModal, showUpdateNodesModal, showDeleteNodesModal, showToggleNodesModal, showAddModelsModal, showDeleteModelsModal, showSetTurnServerCredsModal }; // Also keep the global for backward compatibility window.comfyStreamSettings = { @@ -1976,5 +2241,6 @@ window.comfyStreamSettings = { showDeleteNodesModal, showToggleNodesModal, showAddModelsModal, - showDeleteModelsModal + showDeleteModelsModal, + showSetTurnServerCredsModal }; \ No newline at end of file diff --git a/server/api/api.py b/server/api/api.py index 84243037..42f638c6 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -26,7 +26,7 @@ def add_mgmt_api_routes(app): app.router.add_post("/settings/comfystream/reload", reload) app.router.add_post("/settings/comfyui/restart", restart_comfyui_process) - app.router.add_post("/settings/twilio/set/account", set_account_info) + app.router.add_post("/settings/turn/server/set/account", set_account_info) async def reload(request): From 7d2a4b160ae52677c494209945d0f8cd5b663934 Mon Sep 17 00:00:00 2001 From: Brad P Date: Fri, 18 Apr 2025 12:06:19 -0500 Subject: [PATCH 22/24] fix update node --- server/api/nodes/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/nodes/nodes.py b/server/api/nodes/nodes.py index 44f409d3..8e40984c 100644 --- a/server/api/nodes/nodes.py +++ b/server/api/nodes/nodes.py @@ -97,7 +97,7 @@ async def install_node(node, workspace_dir): if branch == "": branch = repo.remotes.origin.refs[0].remote_head - repo.remotes.origin.pull(branch) + repo.git.checkout(branch) # Install requirements if present requirements_file = node_path / "requirements.txt" From d3993eb969f4e7baaed18cc22fe9c9d41b52a4fb Mon Sep 17 00:00:00 2001 From: Brad P Date: Sun, 27 Apr 2025 06:52:20 -0500 Subject: [PATCH 23/24] add log lines on ComfyUI restart --- nodes/api/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nodes/api/__init__.py b/nodes/api/__init__.py index f5f5532a..22e017c1 100644 --- a/nodes/api/__init__.py +++ b/nodes/api/__init__.py @@ -254,6 +254,7 @@ async def manage_configuration(request): server_status = server_manager.get_status() if server_status["running"]: await server_manager.stop() - + logging.info("Restarting ComfyUI...") subprocess.run(["supervisorctl", "restart", "comfyui"]) + logging.info("Restarting ComfyUI...in process") return web.json_response({"success": True}, status=200) From cca6715f2effc28807ccfe4a0c9204368f9f0d2b Mon Sep 17 00:00:00 2001 From: Brad P Date: Thu, 15 May 2025 21:29:29 -0500 Subject: [PATCH 24/24] fix dependencies and update import --- pyproject.toml | 3 ++- server/api/api.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 96fc96cf..3e8d67a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ dependencies = [ "toml", "twilio", "prometheus_client", - "librosa" + "librosa", + "GitPython" ] [project.optional-dependencies] diff --git a/server/api/api.py b/server/api/api.py index 42f638c6..ac4a623f 100644 --- a/server/api/api.py +++ b/server/api/api.py @@ -4,7 +4,7 @@ from api.nodes.nodes import list_nodes, install_node, delete_node, toggle_node from api.models.models import list_models, add_model, delete_model from api.settings.settings import set_twilio_account_info, restart_comfyui -from pipeline import Pipeline +from comfystream.pipeline import Pipeline from comfy.nodes.package import _comfy_nodes, import_all_nodes_in_workspace