Compare commits

...
33 Commits
Author SHA1 Message Date
bojiang 62029e4a30 chore: Update README.md 2024-12-18 20:35:20 +08:00
bojiang ca97c40d9f doc: Update README.md 2024-12-18 19:10:22 +08:00
bojiang 230e25dbf0 doc: Update banner 2024-12-18 17:29:29 +08:00
bojiang 9347df2159 doc: add banner 2024-12-18 17:08:58 +08:00
bojiang 0d582b6458 doc: Update README.md 2024-12-18 17:01:17 +08:00
Sean Sheng 2878a6fcdf doc: update README.md 2024-12-18 00:55:28 -08:00
bojiang 367ea64dcd chore: add examples 2024-12-18 14:52:28 +08:00
bojiang a6c05d3b48 chore: add init subcommand 2024-12-18 14:51:17 +08:00
bojiang 4bfd08cead chore: revert hf models 2024-12-18 13:17:30 +08:00
bojiang c75539c167 Update README.md 2024-12-18 11:22:21 +08:00
bojiang 962aa5b573 Revert "Revert "fix: don't handle hugging face models specially""
This reverts commit 8a12a9b5fa.
2024-12-18 09:34:32 +08:00
bojiang 068e055501 chore: label comfy-pack version 2024-12-17 19:40:35 +08:00
bojiang feb9c12bd2 make package primary 2024-12-17 19:00:13 +08:00
bojiang 5b10d3dcfb fix: serve 2024-12-17 18:57:56 +08:00
bojiang a58907ed7f chore: enlarge default timeout 2024-12-17 17:04:31 +08:00
bojiang 8a12a9b5fa Revert "fix: don't handle hugging face models specially"
This reverts commit 9daee2c6cc.
2024-12-17 16:34:18 +08:00
bojiang 38cad77c1e fix: model matching 2024-12-17 16:33:16 +08:00
Frost Ming 9daee2c6cc fix: don't handle hugging face models specially
Signed-off-by: Frost Ming <me@frostming.com>
2024-12-17 15:43:16 +08:00
bojiang 4a30cc705b fix: listen address 2024-12-17 15:22:16 +08:00
bojiang 93ca8b49c3 fix: --include-disabled-models 2024-12-16 23:52:21 +08:00
bojiang c30ca2dba4 fix: mkdir if not exist 2024-12-16 23:45:13 +08:00
bojiang e7aed0e765 fix: install custom nodes in venv 2024-12-16 23:43:26 +08:00
bojiang 400fc272eb fix: init custom nodes in venv 2024-12-16 22:39:57 +08:00
bojiang 91ba706b55 fix: verbose 2024-12-16 20:48:51 +08:00
bojiang abbd1f5aaa chore: print verbose level 2024-12-16 20:44:38 +08:00
bojiang 16baab39ef fix: verbose install 2024-12-16 20:38:27 +08:00
bojiang a1624ca69c chore: more log 2024-12-16 20:37:11 +08:00
bojiang 41bae638fc chore: UX 2024-12-16 20:37:11 +08:00
bojiang b7166b6283 Update README.md 2024-12-16 15:51:54 +08:00
bojiang 6abca7db24 add unpack button 2024-12-16 15:49:05 +08:00
Zhao Shenyang e935bad9ac chore: use duckduckgo instead of google for less rate limit (#7) 2024-12-16 08:55:20 +08:00
bojiang 9617b568ed Update README.md 2024-12-14 15:11:56 +08:00
Frost Ming 146b79cf74 feat: register huggingface models
Signed-off-by: Frost Ming <me@frostming.com>
2024-12-13 10:16:44 +08:00
12 changed files with 621 additions and 141 deletions
+83 -28
View File
@@ -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)
![install_node](https://github.com/user-attachments/assets/dbfb730d-edff-4a52-b6c4-695e3ec70368)
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)
![pack](https://github.com/user-attachments/assets/e08bbed2-84dc-474e-a701-6c6db16edf76)
### 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>
![input](https://github.com/user-attachments/assets/44264007-0ac8-4e23-8dc0-e60aa0ebcea2)
![output](https://github.com/user-attachments/assets/a4526661-8930-4575-bacc-33b6887f6271)
</details>
<details>
<summary> 2. serve and test locally </summary>
![serve](https://github.com/user-attachments/assets/8d4c92c5-d6d7-485e-bc71-e4fc0fe8bf35)
</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
![serve](https://github.com/user-attachments/assets/57b5ff75-6109-4f06-99a2-778942030236)
<details>
<summary> 4. (Optional) deploy to cloud * </summary>
## Parameter Nodes
![image](https://github.com/user-attachments/assets/1ffa31fc-1f50-4ea7-a47e-7dae3b874273)
</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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,5 @@
bentoml
fastapi
comfy-cli
googlesearch-python
duckduckgo-search
uv
+257 -36
View File
@@ -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]")
+2
View File
@@ -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"
+36 -27
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 &lt;your-app&gt;.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.');