Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62029e4a30 | ||
|
|
ca97c40d9f | ||
|
|
230e25dbf0 | ||
|
|
9347df2159 | ||
|
|
0d582b6458 | ||
|
|
2878a6fcdf | ||
|
|
367ea64dcd | ||
|
|
a6c05d3b48 | ||
|
|
4bfd08cead | ||
|
|
c75539c167 | ||
|
|
962aa5b573 | ||
|
|
068e055501 | ||
|
|
feb9c12bd2 | ||
|
|
5b10d3dcfb | ||
|
|
a58907ed7f | ||
|
|
8a12a9b5fa | ||
|
|
38cad77c1e | ||
|
|
9daee2c6cc | ||
|
|
4a30cc705b | ||
|
|
93ca8b49c3 | ||
|
|
c30ca2dba4 | ||
|
|
e7aed0e765 | ||
|
|
400fc272eb | ||
|
|
91ba706b55 | ||
|
|
abbd1f5aaa | ||
|
|
16baab39ef | ||
|
|
a1624ca69c | ||
|
|
41bae638fc | ||
|
|
b7166b6283 | ||
|
|
6abca7db24 | ||
|
|
e935bad9ac | ||
|
|
9617b568ed | ||
|
|
146b79cf74 |
@@ -1,60 +1,115 @@
|
||||
# Comfy-Pack
|
||||
# Comfy-Pack: Package and Deploy ComfyUI Workflows
|
||||
> ⚠️ **WARNING**: This project is under active development.
|
||||
|
||||
A comprehensive toolkit for standardizing, packaging and deploying ComfyUI workflows as reproducible environments and production-ready REST services.
|
||||
<img width="952" alt="banner" src="https://github.com/user-attachments/assets/1ab2c7a7-55da-4fa4-a821-db7ba720fbc9" />
|
||||
|
||||
## Features
|
||||
- **Package Everything**: Create reproducible `.cpack.zip` files containing your workflow, custom nodes, model versions, and all dependencies
|
||||
- **Standardize Parameters**: Define and validate workflow inputs through UI nodes for images, text, numbers and more
|
||||
- **CLI Support**: Restore environment and run inference from command line
|
||||
- **REST API Generation**: Auto-convert any workflow into REST service with OpenAPI docs
|
||||
A comprehensive toolkit for reliably packing and unpacking environments for ComfyUI workflows.
|
||||
|
||||
|
||||
- 📦 **Pack workflow environments as artifacts:** Saves the workflow environment in a `.cpack.zip` artifact with Python package versions, ComfyUI and custom node revisions, and model hashes.
|
||||
- ✨ **Unpack artifacts to recreate workflow environments:** Unpacks the `.cpack.zip` artifact to recreate the same environment with the exact Python package versions, ComfyUI and custom node revisions, and model weights.
|
||||
- 🚀 **Deploy workflows as APIs:** Deploys the workflow as a RESTful API with customizable input and output parameters.
|
||||
|
||||
## Motivations
|
||||
|
||||
When sharing ComfyUI workflows to others, your audiences or team member, you've likely heard these responses:
|
||||
- "Custom Node not found"
|
||||
- "Cannot find the model file"
|
||||
- "Missing Python dependencies"
|
||||
|
||||
These are fundamental challenges in workflow sharing – every component must match exactly: custom nodes, model files, and Python dependencies.
|
||||
|
||||
We learned it from our communit, and developed Comfy-Pack to address this directly. With a single click, it captures and locks your entire workflow environment into a `.cpack.zip` file, including Python packages, custom nodes, model hashes, and required assets.
|
||||
|
||||
Users can recreate the exact environment with one command:
|
||||
```bash
|
||||
comfy-pack unpack workflow.cpack.zip
|
||||
```
|
||||
|
||||
Focus on creating. Let Comfy-Pack handle the rest.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
Search `comfy-pack` in ComfyUI Manager (Recommended)
|
||||
|
||||

|
||||
|
||||
or install from Git:
|
||||
|
||||
```bash
|
||||
pip install comfy-pack
|
||||
git clone https://github.com/bentoml/comfy-pack.git
|
||||
```
|
||||
|
||||
|
||||
### Pack a ComfyUI workflow
|
||||
1. Click "Package" button to create `.cpack.zip`
|
||||
2. (Optional) select the models that you want to include (only model hash will be recorded, so you wont get a 100GB zip file)
|
||||

|
||||
|
||||
### Create a Pack
|
||||
1. Search&Install ComfyUI-IDL custom nodes in ComfyUI with Manager
|
||||
2. Design your workflow with comfy-pack input/output nodes
|
||||
3. Click "Package" button to create `.cpack.zip`
|
||||
|
||||
### Restore to a ComfyUI project
|
||||
|
||||
### Unpack a ComfyUI workflow
|
||||
```bash
|
||||
# Restore environment from pack, will install everything needed except the models.
|
||||
comfy-pack restore workflow.cpack.zip --dir ./
|
||||
# Restore the full ComfyUI directory from cpack files.
|
||||
comfy-pack unpack workflow.cpack.zip
|
||||
```
|
||||
|
||||
### Run Inference
|
||||
|
||||
### Deploy a workflow as an API
|
||||
<details>
|
||||
<summary> 1. annotate input & output </summary>
|
||||
|
||||

|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> 2. serve and test locally </summary>
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> 3. (Optional) pack & run anywhere </summary>
|
||||
|
||||
```bash
|
||||
# Get the workflow input spec
|
||||
comfy-pack info workflow.cpack.zip
|
||||
comfy-pack run workflow.cpack.zip --help
|
||||
|
||||
# Run
|
||||
comfy-pack run workflow.cpack.zip --src-image image.png --video video.mp4
|
||||
```
|
||||
</details>
|
||||
|
||||
### Develop REST service
|
||||

|
||||
<details>
|
||||
<summary> 4. (Optional) deploy to cloud * </summary>
|
||||
|
||||
## Parameter Nodes
|
||||

|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Custom Node List
|
||||
|
||||
ComfyPack provides custom nodes for standardizing inputs:
|
||||
- ImageInput
|
||||
- StringInput
|
||||
- IntInput
|
||||
- AnyInput
|
||||
- ImageOutput
|
||||
- FileOutput
|
||||
- ImageInput: provides `image` type input, similar to official `LoadImage` node
|
||||
- StringInput: provides `string` type input, nice for prompts
|
||||
- IntInput: provides `int` type input, suitable for size or seeds
|
||||
- AnyInput: provides `combo` type and more input, suitable for custom nodes
|
||||
- ImageOutput: takes `image` type inputs, similar to official `SaveImage` node, take an image of a bunch of images
|
||||
- FileOutput: takes file path as `string` type, save and output the file under that path
|
||||
- ...
|
||||
|
||||
These nodes help define clear interfaces for your workflow.
|
||||
|
||||
## Docker Support
|
||||
Under development
|
||||
## 🚀 Roadmap
|
||||
- Docker Support
|
||||
- local cpack manager and Version Control
|
||||
- Enhanced service capabilities
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
Binary file not shown.
+8
-1
@@ -20,6 +20,7 @@ from server import PromptServer
|
||||
|
||||
from comfy_pack.hash import async_batch_get_sha256
|
||||
from comfy_pack.model_helper import alookup_model_source
|
||||
from comfy_pack.utils import get_self_git_commit
|
||||
|
||||
ZPath = Union[Path, zipfile.Path]
|
||||
TEMP_FOLDER = Path(__file__).parent.parent / "temp"
|
||||
@@ -136,8 +137,13 @@ async def _get_models(
|
||||
model_data["sha256"],
|
||||
cache_only=not ensure_source,
|
||||
)
|
||||
# should_store = store_models and (
|
||||
# model_data["source"].get("source") != "huggingface"
|
||||
# or model_data["source"].get("repo", "").startswith("datasets/")
|
||||
# ) # TODO: sort this out
|
||||
should_store = store_models
|
||||
|
||||
if store_models:
|
||||
if should_store:
|
||||
import bentoml
|
||||
|
||||
model_tag = f'cpack-model:{model_data["sha256"][:16]}'
|
||||
@@ -511,6 +517,7 @@ async def build_bento(request):
|
||||
"service:ComfyService",
|
||||
name=data["bento_name"],
|
||||
build_ctx=temp_dir,
|
||||
labels={"comfy-pack-version": get_self_git_commit() or "unknown"},
|
||||
models=[m["model_tag"] for m in models if "model_tag" in m],
|
||||
docker={
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||
|
||||
+52
-19
@@ -13,13 +13,13 @@ from typing import Any, cast
|
||||
|
||||
import bentoml
|
||||
import fastapi
|
||||
from bentoml.models import HuggingFaceModel
|
||||
|
||||
import comfy_pack
|
||||
import comfy_pack.run
|
||||
|
||||
REQUEST_TIMEOUT = 360
|
||||
REQUEST_TIMEOUT = 3600
|
||||
BASE_DIR = Path(__file__).parent
|
||||
WORKFLOW_FILE = BASE_DIR / "workflow_api.json"
|
||||
COPY_THRESHOLD = 10 * 1024 * 1024
|
||||
INPUT_DIR = BASE_DIR / "input"
|
||||
logger = logging.getLogger("bentoml.service")
|
||||
@@ -27,8 +27,7 @@ logger = logging.getLogger("bentoml.service")
|
||||
|
||||
EXISTING_COMFYUI_SERVER = os.environ.get("COMFYUI_SERVER")
|
||||
|
||||
|
||||
with open(WORKFLOW_FILE, "r") as f:
|
||||
with BASE_DIR.joinpath("workflow_api.json").open() as f:
|
||||
workflow = json.load(f)
|
||||
|
||||
InputModel = comfy_pack.generate_input_model(workflow)
|
||||
@@ -67,6 +66,14 @@ def _watch_server(server: comfy_pack.run.ComfyUIServer):
|
||||
break
|
||||
|
||||
|
||||
if not EXISTING_COMFYUI_SERVER:
|
||||
# register models
|
||||
with BASE_DIR.joinpath("snapshot.json").open("rb") as f:
|
||||
snapshot = json.load(f)
|
||||
else:
|
||||
snapshot = {}
|
||||
|
||||
|
||||
@bentoml.mount_asgi_app(app, path="/comfy")
|
||||
@bentoml.service(traffic={"timeout": REQUEST_TIMEOUT * 2}, resources={"gpu": 1})
|
||||
class ComfyService:
|
||||
@@ -142,9 +149,6 @@ class ComfyService:
|
||||
verbose = int("BENTOML_DEBUG" in os.environ)
|
||||
comfy_workspace = _get_workspace()
|
||||
|
||||
with BASE_DIR.joinpath("snapshot.json").open("rb") as f:
|
||||
snapshot = json.load(f)
|
||||
|
||||
if not comfy_workspace.joinpath(".DONE").exists():
|
||||
if comfy_workspace.exists():
|
||||
logger.info("Removing existing workspace")
|
||||
@@ -154,19 +158,34 @@ class ComfyService:
|
||||
for model in snapshot["models"]:
|
||||
if model.get("disabled", False):
|
||||
continue
|
||||
model_tag = model.get("model_tag")
|
||||
if not model_tag:
|
||||
logger.warning(
|
||||
"Model %s is not in model store, the workflow may not work",
|
||||
model["filename"],
|
||||
)
|
||||
continue
|
||||
model_path = comfy_workspace / cast(str, model["filename"])
|
||||
model_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
bento_model = bentoml.models.get(model_tag)
|
||||
model_file = bento_model.path_of("model.bin")
|
||||
logger.info("Copying %s to %s", model_file, model_path)
|
||||
model_path.symlink_to(model_file)
|
||||
if model_tag := model.get("model_tag"):
|
||||
model_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
bento_model = bentoml.models.get(model_tag)
|
||||
model_file = bento_model.path_of("model.bin")
|
||||
logger.info("Copying %s to %s", model_file, model_path)
|
||||
model_path.symlink_to(model_file)
|
||||
elif (source := model["source"]).get("source") == "huggingface":
|
||||
matched = next(
|
||||
(
|
||||
m
|
||||
for m in ComfyService.models
|
||||
if isinstance(m, HuggingFaceModel)
|
||||
and m.model_id.lower() == source["repo"].lower()
|
||||
and source["commit"].lower() == m.revision.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if matched is not None:
|
||||
model_file = os.path.join(matched.resolve(), source["path"])
|
||||
model_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
logger.info("Copying %s to %s", model_file, model_path)
|
||||
model_path.symlink_to(model_file)
|
||||
else:
|
||||
logger.warning(
|
||||
"Unrecognized model source: %s, the model may be missing",
|
||||
source,
|
||||
)
|
||||
|
||||
for f in INPUT_DIR.glob("*"):
|
||||
if f.is_file():
|
||||
@@ -176,3 +195,17 @@ class ComfyService:
|
||||
|
||||
install_custom_modules(snapshot, comfy_workspace, verbose=verbose)
|
||||
comfy_workspace.joinpath(".DONE").touch()
|
||||
|
||||
|
||||
if not EXISTING_COMFYUI_SERVER:
|
||||
for model in snapshot["models"]:
|
||||
if True:
|
||||
continue
|
||||
if model.get("disabled"):
|
||||
continue
|
||||
source = model["source"]
|
||||
if source.get("source") != "huggingface" or source["repo"].startswith(
|
||||
"datasets/"
|
||||
):
|
||||
continue
|
||||
ComfyService.models.append(HuggingFaceModel(source["repo"], source["commit"]))
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
bentoml
|
||||
fastapi
|
||||
comfy-cli
|
||||
googlesearch-python
|
||||
duckduckgo-search
|
||||
uv
|
||||
|
||||
+257
-36
@@ -3,27 +3,238 @@ import functools
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from .const import WORKSPACE_DIR
|
||||
from .const import WORKSPACE_DIR, COMFYUI_REPO, COMFY_PACK_REPO, COMFYUI_MANAGER_REPO
|
||||
from .hash import get_sha256
|
||||
from .utils import get_self_git_commit
|
||||
|
||||
|
||||
def _ensure_uv() -> None:
|
||||
"""Ensure uv is installed, raise error if not."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["uv", "--version"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
raise RuntimeError(
|
||||
"uv is not installed. Please install it first:\n"
|
||||
"curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
)
|
||||
|
||||
|
||||
@click.group()
|
||||
def main():
|
||||
"""comfy-pack CLI"""
|
||||
pass
|
||||
|
||||
|
||||
@main.command(
|
||||
name="init",
|
||||
help="Install latest ComfyUI and comfy-pack custom nodes and create a virtual environment",
|
||||
)
|
||||
@click.option(
|
||||
"--dir",
|
||||
"-d",
|
||||
default="ComfyUI",
|
||||
help="Target directory to install ComfyUI",
|
||||
type=click.Path(file_okay=False),
|
||||
)
|
||||
@click.option(
|
||||
"--verbose",
|
||||
"-v",
|
||||
count=True,
|
||||
help="Increase verbosity level (use multiple times for more verbosity)",
|
||||
help="Increase verbosity level",
|
||||
)
|
||||
@click.pass_context
|
||||
def main(ctx, verbose):
|
||||
"""ComfyUI Pack CLI"""
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["verbose"] = verbose
|
||||
def init(dir: str, verbose: int):
|
||||
from rich.console import Console
|
||||
import os
|
||||
|
||||
console = Console()
|
||||
|
||||
# Check if directory path is valid
|
||||
try:
|
||||
install_dir = Path(dir).absolute()
|
||||
if install_dir.exists() and not install_dir.is_dir():
|
||||
console.print(f"[red]Error: {dir} exists but is not a directory[/red]")
|
||||
return 1
|
||||
|
||||
# Check if directory is empty or contains ComfyUI
|
||||
if install_dir.exists():
|
||||
contents = list(install_dir.iterdir())
|
||||
if contents and not (install_dir / ".git").exists():
|
||||
console.print(
|
||||
f"[red]Error: Directory {dir} is not empty and doesn't appear to be a ComfyUI installation[/red]"
|
||||
)
|
||||
return 1
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error: Invalid directory path - {str(e)}[/red]")
|
||||
return 1
|
||||
|
||||
# Check git installation
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "--version"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
console.print("[red]Error: git is not installed or not in PATH[/red]")
|
||||
return 1
|
||||
|
||||
# Check if we have write permissions
|
||||
try:
|
||||
if not install_dir.exists():
|
||||
install_dir.mkdir(parents=True)
|
||||
test_file = install_dir / ".write_test"
|
||||
test_file.touch()
|
||||
test_file.unlink()
|
||||
except (OSError, PermissionError) as e:
|
||||
console.print(f"[red]Error: No write permission in {dir} - {str(e)}[/red]")
|
||||
return 1
|
||||
|
||||
# Check if Python version is compatible
|
||||
if sys.version_info < (3, 8):
|
||||
console.print("[red]Error: Python 3.8 or higher is required[/red]")
|
||||
return 1
|
||||
|
||||
# Check if uv is installed
|
||||
try:
|
||||
_ensure_uv()
|
||||
except RuntimeError as e:
|
||||
console.print(f"[red]Error: {str(e)}[/red]")
|
||||
return 1
|
||||
|
||||
# Check if enough disk space is available (rough estimate: 2GB)
|
||||
try:
|
||||
free_space = shutil.disk_usage(install_dir).free
|
||||
if free_space < 2 * 1024 * 1024 * 1024: # 2GB in bytes
|
||||
console.print(
|
||||
"[yellow]Warning: Less than 2GB free disk space available[/yellow]"
|
||||
)
|
||||
except Exception as e:
|
||||
console.print(
|
||||
f"[yellow]Warning: Could not check free disk space - {str(e)}[/yellow]"
|
||||
)
|
||||
|
||||
# Clone ComfyUI if not exists
|
||||
if not (install_dir / ".git").exists():
|
||||
console.print("[green]Cloning ComfyUI...[/green]")
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"clone",
|
||||
COMFYUI_REPO,
|
||||
str(install_dir),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Update ComfyUI
|
||||
console.print("[green]Updating ComfyUI...[/green]")
|
||||
subprocess.run(
|
||||
["git", "pull"],
|
||||
cwd=install_dir,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Create and activate venv
|
||||
venv_dir = install_dir / ".venv"
|
||||
console.print("[green]Creating virtual environment with uv...[/green]")
|
||||
if venv_dir.exists():
|
||||
shutil.rmtree(venv_dir)
|
||||
subprocess.run(
|
||||
["uv", "venv", str(venv_dir)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Get python path for future use
|
||||
if sys.platform == "win32":
|
||||
python = str(venv_dir / "Scripts" / "python.exe")
|
||||
|
||||
else:
|
||||
python = str(venv_dir / "bin" / "python")
|
||||
|
||||
# Install requirements with uv
|
||||
console.print("[green]Installing ComfyUI requirements with uv...[/green]")
|
||||
subprocess.run(
|
||||
["uv", "pip", "install", "pip", "--upgrade"],
|
||||
env={
|
||||
"VIRTUAL_ENV": str(venv_dir),
|
||||
"PATH": str(venv_dir / "bin") + os.pathsep + os.environ["PATH"],
|
||||
},
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["uv", "pip", "install", "-r", str(install_dir / "requirements.txt")],
|
||||
env={
|
||||
"VIRTUAL_ENV": str(venv_dir),
|
||||
"PATH": str(venv_dir / "bin") + os.pathsep + os.environ["PATH"],
|
||||
},
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Install comfy-pack as custom node
|
||||
console.print("[green]Installing comfy-pack custom nodes...[/green]")
|
||||
custom_nodes_dir = install_dir / "custom_nodes"
|
||||
custom_nodes_dir.mkdir(exist_ok=True)
|
||||
|
||||
comfyui_manager_dir = custom_nodes_dir / "ComfyUI-Manager"
|
||||
if not (comfyui_manager_dir / ".git").exists():
|
||||
# Clone ComfyUI-Manager
|
||||
subprocess.run(
|
||||
["git", "clone", COMFYUI_MANAGER_REPO, str(comfyui_manager_dir)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
comfy_pack_dir = custom_nodes_dir / "comfy-pack"
|
||||
if not (comfy_pack_dir / ".git").exists():
|
||||
# Clone comfy-pack
|
||||
subprocess.run(
|
||||
["git", "clone", COMFY_PACK_REPO, str(comfy_pack_dir)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Update comfy-pack
|
||||
subprocess.run(
|
||||
["git", "pull"],
|
||||
cwd=comfy_pack_dir,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Install comfy-pack requirements
|
||||
if (comfy_pack_dir / "requirements.txt").exists():
|
||||
subprocess.run(
|
||||
[
|
||||
python,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-r",
|
||||
str(comfy_pack_dir / "requirements.txt"),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
version = get_self_git_commit() or "unknown"
|
||||
console.print(
|
||||
f"\n[green]✓ Installation completed! (comfy-pack version: {version})[/green]"
|
||||
)
|
||||
console.print(f"ComfyUI directory: {install_dir}")
|
||||
|
||||
console.print(
|
||||
"\n[green]Next steps:[/green]\n"
|
||||
f"1. cd {dir}\n"
|
||||
"2. source .venv/bin/activate # On Windows: .venv\\Scripts\\activate\n"
|
||||
"3. python main.py"
|
||||
)
|
||||
|
||||
|
||||
@main.command(
|
||||
name="restore",
|
||||
name="unpack",
|
||||
help="Restore the ComfyUI workspace to specified directory",
|
||||
)
|
||||
@click.argument("cpack", type=click.Path(exists=True, dir_okay=False))
|
||||
@@ -34,14 +245,25 @@ def main(ctx, verbose):
|
||||
help="target directory to restore the ComfyUI project",
|
||||
type=click.Path(file_okay=False),
|
||||
)
|
||||
@click.pass_context
|
||||
def restore_cmd(ctx, cpack: str, dir: str):
|
||||
@click.option(
|
||||
"--include-disabled-models",
|
||||
default=False,
|
||||
type=click.BOOL,
|
||||
is_flag=True,
|
||||
)
|
||||
@click.option(
|
||||
"--verbose",
|
||||
"-v",
|
||||
count=True,
|
||||
help="Increase verbosity level (use multiple times for more verbosity)",
|
||||
)
|
||||
def unpack_cmd(cpack: str, dir: str, include_disabled_models: bool, verbose: int):
|
||||
from .package import install
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
install(cpack, dir, ctx.obj["verbose"])
|
||||
install(cpack, dir, verbose=verbose, all_models=include_disabled_models)
|
||||
console.print("\n[green]✓ ComfyUI Workspace is restored![/green]")
|
||||
console.print(f"{dir}")
|
||||
|
||||
@@ -88,27 +310,6 @@ def _print_schema(schema, verbose: int = 0):
|
||||
Console().print(table)
|
||||
|
||||
|
||||
@main.command(name="info")
|
||||
@click.argument("cpack", type=click.Path(exists=True, dir_okay=False))
|
||||
@click.pass_context
|
||||
def info_cmd(ctx, cpack: str):
|
||||
"""
|
||||
Display information about the ComfyUI package.
|
||||
|
||||
Example:
|
||||
$ comfy_pack info workspace.cpack.zip
|
||||
"""
|
||||
from .utils import generate_input_model
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
pack_dir = Path(temp_dir) / ".cpack"
|
||||
shutil.unpack_archive(cpack, pack_dir)
|
||||
workflow = json.loads((pack_dir / "workflow_api.json").read_text())
|
||||
|
||||
inputs = generate_input_model(workflow)
|
||||
_print_schema(inputs.model_json_schema(), ctx.obj["verbose"])
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def _get_cache_workspace(cpack: str):
|
||||
sha = get_sha256(cpack)
|
||||
@@ -121,11 +322,19 @@ def _get_cache_workspace(cpack: str):
|
||||
"allow_extra_args": True,
|
||||
},
|
||||
help="Run a ComfyUI package with the given inputs",
|
||||
add_help_option=False,
|
||||
)
|
||||
@click.argument("cpack", type=click.Path(exists=True, dir_okay=False))
|
||||
@click.option("--output-dir", "-o", type=click.Path(), default=".")
|
||||
@click.option("--help", "-h", is_flag=True, help="Show this message and input schema")
|
||||
@click.option(
|
||||
"--verbose",
|
||||
"-v",
|
||||
count=True,
|
||||
help="Increase verbosity level (use multiple times for more verbosity)",
|
||||
)
|
||||
@click.pass_context
|
||||
def run(ctx, cpack: str, output_dir: str):
|
||||
def run(ctx, cpack: str, output_dir: str, help: bool, verbose: int):
|
||||
from .utils import generate_input_model
|
||||
from pydantic import ValidationError
|
||||
from rich.console import Console
|
||||
@@ -143,6 +352,15 @@ def run(ctx, cpack: str, output_dir: str):
|
||||
|
||||
input_model = generate_input_model(workflow)
|
||||
|
||||
# If help is requested, show command help and input schema
|
||||
if help:
|
||||
console.print(
|
||||
'Usage: comfy-pack run [OPTIONS] CPACK --input1 "value1" --input2 "value2" ...'
|
||||
)
|
||||
console.print("Run a ComfyUI package with the given inputs:")
|
||||
_print_schema(input_model.model_json_schema(), verbose)
|
||||
return 0
|
||||
|
||||
try:
|
||||
validated_data = input_model(**inputs)
|
||||
console.print("[green]✓ Input is valid![/green]")
|
||||
@@ -152,6 +370,9 @@ def run(ctx, cpack: str, output_dir: str):
|
||||
console.print("[red]✗ Validation failed![/red]")
|
||||
for error in e.errors():
|
||||
console.print(f"- {error['loc'][0]}: {error['msg']}")
|
||||
|
||||
console.print("\n[yellow]Expected inputs:[/yellow]")
|
||||
_print_schema(input_model.model_json_schema(), verbose)
|
||||
return 1
|
||||
|
||||
from .package import install
|
||||
@@ -161,7 +382,7 @@ def run(ctx, cpack: str, output_dir: str):
|
||||
console.print("\n[green]✓ Restoring ComfyUI Workspace...[/green]")
|
||||
if workspace.exists():
|
||||
shutil.rmtree(workspace)
|
||||
install(cpack, workspace)
|
||||
install(cpack, workspace, verbose=verbose)
|
||||
with open(workspace / "DONE", "w") as f:
|
||||
f.write("DONE")
|
||||
console.print("\n[green]✓ ComfyUI Workspace is restored![/green]")
|
||||
@@ -169,14 +390,14 @@ def run(ctx, cpack: str, output_dir: str):
|
||||
|
||||
from .run import ComfyUIServer, run_workflow
|
||||
|
||||
with ComfyUIServer(str(workspace.absolute()), verbose=ctx.obj["verbose"]) as server:
|
||||
with ComfyUIServer(str(workspace.absolute()), verbose=verbose) as server:
|
||||
console.print("\n[green]✓ ComfyUI is launched in the background![/green]")
|
||||
results = run_workflow(
|
||||
server.host,
|
||||
server.port,
|
||||
workflow,
|
||||
Path(output_dir).absolute(),
|
||||
verbose=ctx.obj["verbose"],
|
||||
verbose=verbose,
|
||||
**validated_data.model_dump(),
|
||||
)
|
||||
console.print("\n[green]✓ Workflow is executed successfully![/green]")
|
||||
|
||||
@@ -16,3 +16,5 @@ SHA_CACHE_FILE = CPACK_HOME / "sha_cache.json"
|
||||
MODEL_SOURCE_CACHE_FILE = CPACK_HOME / "model_source_cache.json"
|
||||
|
||||
COMFYUI_REPO = "https://github.com/comfyanonymous/ComfyUI.git"
|
||||
COMFY_PACK_REPO = "https://github.com/bentoml/comfy-pack.git"
|
||||
COMFYUI_MANAGER_REPO = "https://github.com/ltdrdata/ComfyUI-Manager.git"
|
||||
|
||||
@@ -14,37 +14,46 @@ PATH_PATTERN = re.compile(
|
||||
|
||||
|
||||
async def _lookup_huggingface_model(model_sha: str) -> dict:
|
||||
from googlesearch import search
|
||||
from duckduckgo_search import DDGS
|
||||
import aiohttp
|
||||
|
||||
query = f"site:huggingface.co blob {model_sha}"
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
for url in search(query, num_results=5):
|
||||
assert isinstance(url, str)
|
||||
if "blob" not in url:
|
||||
continue
|
||||
try:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
|
||||
try:
|
||||
with DDGS() as ddgs:
|
||||
search_results = ddgs.text(query, max_results=5)
|
||||
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
for result in search_results:
|
||||
url = result['link']
|
||||
if "blob" not in url:
|
||||
continue
|
||||
text = await resp.text()
|
||||
if commit_match := COMMIT_PATTERN.search(text):
|
||||
repo, commit = commit_match.groups()
|
||||
if path_match := PATH_PATTERN.search(text):
|
||||
path = path_match.group(1)
|
||||
download_url = f"https://huggingface.co/{repo}/resolve/{commit}/{path}?download=true"
|
||||
url = f"https://huggingface.co/{repo}/blob/{commit}/{path}"
|
||||
info = {
|
||||
"download_url": download_url,
|
||||
"url": url,
|
||||
"repo": repo,
|
||||
"commit": commit,
|
||||
"path": path,
|
||||
"source": "huggingface",
|
||||
}
|
||||
return info
|
||||
except aiohttp.ClientError:
|
||||
continue
|
||||
|
||||
try:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
continue
|
||||
text = await resp.text()
|
||||
if commit_match := COMMIT_PATTERN.search(text):
|
||||
repo, commit = commit_match.groups()
|
||||
if path_match := PATH_PATTERN.search(text):
|
||||
path = path_match.group(1)
|
||||
download_url = f"https://huggingface.co/{repo}/resolve/{commit}/{path}?download=true"
|
||||
url = f"https://huggingface.co/{repo}/blob/{commit}/{path}"
|
||||
info = {
|
||||
"download_url": download_url,
|
||||
"url": url,
|
||||
"repo": repo,
|
||||
"commit": commit,
|
||||
"path": path,
|
||||
"source": "huggingface",
|
||||
}
|
||||
return info
|
||||
except aiohttp.ClientError:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
+35
-11
@@ -76,6 +76,7 @@ def install_custom_modules(snapshot, workspace: Path, verbose: int = 0):
|
||||
_clone_commit(url, commit_hash, module_dir, verbose=verbose)
|
||||
|
||||
if module_dir.joinpath("install.py").exists():
|
||||
env = os.environ.copy()
|
||||
venv = workspace / ".venv"
|
||||
if venv.exists():
|
||||
python = (
|
||||
@@ -83,8 +84,17 @@ def install_custom_modules(snapshot, workspace: Path, verbose: int = 0):
|
||||
if os.name == "nt"
|
||||
else venv / "bin" / "python"
|
||||
)
|
||||
if "PATH" in env:
|
||||
env["PATH"] = f"{str(python.parent)}:{env['PATH']}"
|
||||
else:
|
||||
env["PATH"] = str(python.parent)
|
||||
env["VIRTUAL_ENV"] = str(venv)
|
||||
else:
|
||||
python = Path(sys.executable)
|
||||
|
||||
if verbose > 0:
|
||||
print(f"Installing {directory} custom node")
|
||||
print(f"$ {python.absolute()} install.py")
|
||||
subprocess.check_call(
|
||||
[str(python.absolute()), "install.py"],
|
||||
cwd=module_dir,
|
||||
@@ -101,8 +111,7 @@ def install_dependencies(
|
||||
workspace: Path,
|
||||
verbose: int = 0,
|
||||
):
|
||||
if verbose > 0:
|
||||
print("Installing Python dependencies")
|
||||
print("Installing Python dependencies")
|
||||
python_version = snapshot["python"]
|
||||
stdout = None if verbose > 0 else subprocess.DEVNULL
|
||||
stderr = None if verbose > 1 else subprocess.DEVNULL
|
||||
@@ -142,6 +151,8 @@ def install_dependencies(
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
if verbose > 0:
|
||||
print(f"Installing dependencies from {req_file}")
|
||||
subprocess.check_call(
|
||||
[
|
||||
"uv",
|
||||
@@ -161,9 +172,9 @@ def install_dependencies(
|
||||
return venv_py
|
||||
|
||||
|
||||
def get_google_search_url(sha: str) -> str:
|
||||
"""Generate Google custom search URLs for model on HuggingFace and CivitAI"""
|
||||
base_url = "https://www.google.com/search"
|
||||
def get_search_url(sha: str) -> str:
|
||||
"""Generate custom search URLs for model on HuggingFace and CivitAI"""
|
||||
base_url = "https://duckduckgo.com"
|
||||
sha = sha.upper()
|
||||
hf_query = f"{sha} OR {sha[:10]}"
|
||||
hf_query = urllib.parse.quote(hf_query)
|
||||
@@ -231,9 +242,11 @@ def retrive_models(
|
||||
snapshot: dict,
|
||||
workspace: Path,
|
||||
download: bool = True,
|
||||
all_models: bool = False,
|
||||
verbose: int = 0,
|
||||
):
|
||||
"""Retrieve models from user downloads"""
|
||||
print("Retrieving models")
|
||||
models = snapshot.get("models", [])
|
||||
if not models:
|
||||
return
|
||||
@@ -255,7 +268,7 @@ def retrive_models(
|
||||
create_model_symlink(MODEL_DIR, sha, workspace, filename)
|
||||
continue
|
||||
|
||||
if disabled:
|
||||
if disabled and not all_models:
|
||||
continue
|
||||
|
||||
if not download:
|
||||
@@ -284,8 +297,8 @@ def retrive_models(
|
||||
else:
|
||||
continue
|
||||
|
||||
google_url = get_google_search_url(sha)
|
||||
print(f"Search URL: {google_url}")
|
||||
search_url = get_search_url(sha)
|
||||
print(f"Search URL: {search_url}")
|
||||
print(f"Path: {workspace / filename}")
|
||||
|
||||
while True:
|
||||
@@ -354,9 +367,11 @@ def install(
|
||||
cpack: str | Path,
|
||||
workspace: str | Path = "workspace",
|
||||
preheat: bool = True,
|
||||
all_models: bool = False,
|
||||
verbose: int = 0,
|
||||
):
|
||||
workspace = Path(workspace)
|
||||
print(f"Installing package {cpack} to {workspace} (verbose={verbose})")
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
pack_dir = Path(temp_dir) / ".cpack"
|
||||
shutil.unpack_archive(cpack, pack_dir)
|
||||
@@ -372,13 +387,22 @@ def install(
|
||||
elif f.is_dir():
|
||||
shutil.copytree(f, workspace / "input" / f.name, dirs_exist_ok=True)
|
||||
|
||||
retrive_models(snapshot, workspace, verbose=verbose, download=False)
|
||||
retrive_models(
|
||||
snapshot,
|
||||
workspace,
|
||||
verbose=verbose,
|
||||
download=False,
|
||||
)
|
||||
|
||||
install_custom_modules(snapshot, workspace, verbose=verbose)
|
||||
if preheat:
|
||||
from .run import ComfyUIServer
|
||||
|
||||
with ComfyUIServer(str(workspace), verbose=verbose):
|
||||
with ComfyUIServer(
|
||||
str(workspace),
|
||||
verbose=verbose,
|
||||
venv=str(workspace / ".venv"),
|
||||
) as _:
|
||||
pass
|
||||
|
||||
retrive_models(snapshot, workspace, verbose=verbose)
|
||||
retrive_models(snapshot, workspace, verbose=verbose, all_models=all_models)
|
||||
|
||||
+19
-4
@@ -54,6 +54,7 @@ class ComfyUIServer:
|
||||
input_dir: str | None = None,
|
||||
host: str = "localhost",
|
||||
port: int | None = None,
|
||||
venv: str | None = None,
|
||||
verbose: int = 0,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -69,10 +70,13 @@ class ComfyUIServer:
|
||||
self.server_proc: subprocess.Popen | None = None
|
||||
self._children_procs: list[psutil.Process] = []
|
||||
|
||||
run_dir = Path(workspace) / "cli_run"
|
||||
run_dir = (Path(workspace) / "cli_run").absolute()
|
||||
self.temp_dir = run_dir / "temp"
|
||||
self.output_dir = run_dir / "output"
|
||||
|
||||
self.temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if port is None:
|
||||
if _is_port_in_use(8188):
|
||||
self.port = port if port else random.randint(58000, 58999)
|
||||
@@ -80,6 +84,7 @@ class ComfyUIServer:
|
||||
self.port = 8188
|
||||
else:
|
||||
self.port = port
|
||||
self.venv = venv
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
@@ -100,9 +105,17 @@ class ComfyUIServer:
|
||||
self.temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
env = os.environ.copy()
|
||||
if self.venv:
|
||||
env["VIRTUAL_ENV"] = self.venv
|
||||
if os.name == "nt":
|
||||
env["PATH"] = f"{self.venv}\\Scripts;{env.get('PATH', '')}"
|
||||
else:
|
||||
env["PATH"] = f"{self.venv}/bin:{env.get('PATH', '')}"
|
||||
|
||||
stdout = None if self.verbose > 0 else subprocess.DEVNULL
|
||||
command = ["comfy", "--skip-prompt", "tracking", "disable"]
|
||||
subprocess.run(command, check=True, stdout=stdout)
|
||||
subprocess.run(command, check=True, stdout=stdout, env=env)
|
||||
logger.info("Successfully disabled Comfy CLI tracking")
|
||||
|
||||
logger.info("Preparing directories required by ComfyUI...")
|
||||
@@ -120,12 +133,13 @@ class ComfyUIServer:
|
||||
self.temp_dir,
|
||||
"--port",
|
||||
str(self.port),
|
||||
"--listen",
|
||||
self.host,
|
||||
]
|
||||
if self.input_dir:
|
||||
command.extend(["--input-directory", self.input_dir])
|
||||
|
||||
if self.host != "localhost":
|
||||
command.extend(["--listen", self.host])
|
||||
|
||||
def preexec_fn():
|
||||
os.setpgrp()
|
||||
|
||||
@@ -134,6 +148,7 @@ class ComfyUIServer:
|
||||
stdout=stdout,
|
||||
stderr=None,
|
||||
preexec_fn=preexec_fn,
|
||||
env=env,
|
||||
)
|
||||
|
||||
if _wait_for_startup(self.host, self.port):
|
||||
|
||||
+43
-1
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -258,3 +259,44 @@ def retrieve_workflow_outputs(
|
||||
if len(outs) == 1:
|
||||
return outs[0]
|
||||
return outs
|
||||
|
||||
|
||||
def get_self_git_commit() -> str | None:
|
||||
"""Get current git commit of the repository.
|
||||
|
||||
Returns:
|
||||
str | None: Git commit hash in format "{hash}[-dirty]" or None if not in a git repo
|
||||
"""
|
||||
try:
|
||||
repo_root = Path(__file__).parent.parent.parent
|
||||
|
||||
# Check if we're in a git repo
|
||||
subprocess.run(
|
||||
["git", "rev-parse", "--git-dir"],
|
||||
cwd=repo_root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
# Get current commit hash
|
||||
commit_hash = subprocess.run(
|
||||
["git", "rev-parse", "--short", "HEAD"],
|
||||
cwd=repo_root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
|
||||
# Check if working directory is clean
|
||||
is_dirty = (
|
||||
subprocess.run(
|
||||
["git", "diff", "--quiet"],
|
||||
cwd=repo_root,
|
||||
check=False,
|
||||
).returncode
|
||||
!= 0
|
||||
)
|
||||
|
||||
return f"{commit_hash}-dirty" if is_dirty else commit_hash
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
+84
-13
@@ -403,6 +403,59 @@ function createDownloadModal() {
|
||||
};
|
||||
}
|
||||
|
||||
async function unpackAction() {
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "cpack-modal";
|
||||
modal.style.width = "500px";
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.textContent = "Unpack Instructions";
|
||||
title.className = "cpack-title";
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.innerHTML = `
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="margin-bottom: 15px;">To unpack a workflow package, follow these steps:</p>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<p style="margin-bottom: 10px;">1. Install comfy-pack CLI:</p>
|
||||
<div class="cpack-copyable" style="margin: 10px 0;">
|
||||
<span style="font-family: monospace;">pip install comfy-pack</span>
|
||||
<button class="cpack-btn" onclick="navigator.clipboard.writeText('pip install comfy-pack')" style="padding: 2px 8px;">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p style="margin-bottom: 10px;">2. Run the unpack command:</p>
|
||||
<div class="cpack-copyable" style="margin: 10px 0;">
|
||||
<span style="font-family: monospace;">comfy-pack unpack <your-app>.cpack.zip</span>
|
||||
<button class="cpack-btn" onclick="navigator.clipboard.writeText('comfy-pack unpack')" style="padding: 2px 8px;">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.className = "cpack-btn-container";
|
||||
|
||||
const closeButton = document.createElement("button");
|
||||
closeButton.textContent = "Close";
|
||||
closeButton.className = "cpack-btn";
|
||||
|
||||
buttonContainer.appendChild(closeButton);
|
||||
|
||||
modal.appendChild(title);
|
||||
modal.appendChild(content);
|
||||
modal.appendChild(buttonContainer);
|
||||
|
||||
const { close } = createModal(modal);
|
||||
closeButton.onclick = close;
|
||||
}
|
||||
|
||||
async function packageAction() {
|
||||
if (document.getElementById("input-modal")) return;
|
||||
if (document.getElementById("download-modal")) return;
|
||||
@@ -896,15 +949,21 @@ app.registerExtension({
|
||||
separator.style.width = "100%";
|
||||
menu.append(separator);
|
||||
|
||||
const packButton = document.createElement("button");
|
||||
packButton.textContent = "Package";
|
||||
packButton.onclick = packageAction;
|
||||
menu.append(packButton);
|
||||
|
||||
const unpackButton = document.createElement("button");
|
||||
unpackButton.textContent = "Unpack";
|
||||
unpackButton.onclick = unpackAction;
|
||||
menu.append(unpackButton);
|
||||
|
||||
const serveButton = document.createElement("button");
|
||||
serveButton.textContent = "Serve";
|
||||
serveButton.onclick = serveAction;
|
||||
menu.append(serveButton);
|
||||
|
||||
const packButton = document.createElement("button");
|
||||
packButton.textContent = "Package";
|
||||
packButton.onclick = packageAction;
|
||||
menu.append(packButton);
|
||||
|
||||
const buildButton = document.createElement("button");
|
||||
buildButton.textContent = "Deploy";
|
||||
@@ -916,7 +975,26 @@ app.registerExtension({
|
||||
// new style Manager buttons
|
||||
|
||||
// unload models button into new style Manager button
|
||||
let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup(
|
||||
let cmGroup1 = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup(
|
||||
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
|
||||
icon: "package-variant-closed",
|
||||
action: packageAction,
|
||||
tooltip: "Comfy-Pack",
|
||||
content: "Package",
|
||||
classList: "comfyui-button comfyui-menu-mobile-collapse primary"
|
||||
}).element,
|
||||
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
|
||||
icon: "package-variant",
|
||||
action: unpackAction,
|
||||
tooltip: "Comfy-Pack",
|
||||
content: "Unpack",
|
||||
classList: "comfyui-button comfyui-menu-mobile-collapse"
|
||||
}).element,
|
||||
);
|
||||
|
||||
app.menu?.settingsGroup.element.before(cmGroup1.element);
|
||||
|
||||
let cmGroup2 = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup(
|
||||
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
|
||||
icon: "api",
|
||||
action: serveAction,
|
||||
@@ -924,13 +1002,6 @@ app.registerExtension({
|
||||
content: "Serve",
|
||||
classList: "comfyui-button comfyui-menu-mobile-collapse primary"
|
||||
}).element,
|
||||
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
|
||||
icon: "package-variant-closed",
|
||||
action: packageAction,
|
||||
tooltip: "Comfy-Pack",
|
||||
content: "Package",
|
||||
classList: "comfyui-button comfyui-menu-mobile-collapse"
|
||||
}).element,
|
||||
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
|
||||
icon: "cloud-upload",
|
||||
action: deployAction,
|
||||
@@ -940,7 +1011,7 @@ app.registerExtension({
|
||||
}).element,
|
||||
);
|
||||
|
||||
app.menu?.settingsGroup.element.before(cmGroup.element);
|
||||
app.menu?.settingsGroup.element.before(cmGroup2.element);
|
||||
}
|
||||
catch(exception) {
|
||||
console.log('ComfyUI is outdated. New style menu based features are disabled.');
|
||||
|
||||
Reference in New Issue
Block a user