Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e5a29a3f1 | ||
|
|
f6552c551b | ||
|
|
ef4c24eced | ||
|
|
bb9844db5b | ||
|
|
fe60c8aa2b | ||
|
|
9f73c4b29f | ||
|
|
06cab6a0a1 | ||
|
|
4a22f94071 | ||
|
|
17e2fe77f3 | ||
|
|
06c122af0d | ||
|
|
2c9b65c2c5 | ||
|
|
38a8427970 | ||
|
|
8063b7b60f | ||
|
|
9ae75d5ed7 |
@@ -161,3 +161,5 @@ cython_debug/
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
/temp/
|
||||
src/.DS_Store
|
||||
.DS_Store
|
||||
|
||||
@@ -73,6 +73,13 @@ To unpack:
|
||||
comfy-pack unpack workflow.cpack.zip
|
||||
```
|
||||
|
||||
Huggingface gated models can be accessed by setting your `HF_TOKEN` as an environment variable before unpacking:
|
||||
|
||||
```bash
|
||||
export HF_TOKEN=hf_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx
|
||||
comfy-pack unpack workflow.cpack.zip
|
||||
```
|
||||
|
||||
For example cpack files, check our [examples folder](examples/).
|
||||
|
||||
### Deploy a workflow as an API
|
||||
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
import sys
|
||||
|
||||
SRC_DIR = pathlib.Path(__file__).parent.parent / "src"
|
||||
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.append(str(SRC_DIR))
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
+52
-93
@@ -11,7 +11,7 @@ import tempfile
|
||||
import time
|
||||
import uuid
|
||||
import zipfile
|
||||
from importlib.metadata import Distribution, distributions
|
||||
from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
|
||||
@@ -26,26 +26,7 @@ from comfy_pack.package import build_bento
|
||||
ZPath = Union[Path, zipfile.Path]
|
||||
TEMP_FOLDER = Path(__file__).parent.parent / "temp"
|
||||
COMFY_PACK_DIR = Path(__file__).parent.parent / "src" / "comfy_pack"
|
||||
EXCLUDE_PACKAGES = ["bentoml", "onnxruntime", "conda"] # TODO: standardize this
|
||||
|
||||
|
||||
def _get_requirement_string(dist: Distribution) -> str:
|
||||
direct_url_text = dist.read_text("direct_url.json")
|
||||
pinned_str = f"{dist.metadata['Name']}=={dist.version}"
|
||||
if not direct_url_text:
|
||||
return pinned_str
|
||||
direct_url = json.loads(direct_url_text)
|
||||
if url := direct_url.get("url"):
|
||||
if url.startswith("file://"):
|
||||
# we are not able to share local files
|
||||
return pinned_str
|
||||
if vcs_info := direct_url.get("vcs_info"):
|
||||
url = f"{vcs_info['vcs']}+{url}@{vcs_info['commit_id']}"
|
||||
if subdirectory := direct_url.get("subdirectory"):
|
||||
url += f"#subdirectory={subdirectory}"
|
||||
return f"{dist.metadata['Name']} @ {url}"
|
||||
else:
|
||||
return pinned_str
|
||||
EXCLUDE_PACKAGES = ["bentoml", "onnxruntime", "conda", "nvidia-*"]
|
||||
|
||||
|
||||
def normalize_name(name: str) -> str:
|
||||
@@ -54,36 +35,54 @@ def normalize_name(name: str) -> str:
|
||||
return re.sub(r"[-_.]+", "-", name).lower()
|
||||
|
||||
|
||||
async def _write_requirements(path: ZPath, extras: list[str] | None = None) -> None:
|
||||
print("Package => Writing requirements.txt")
|
||||
seen_packages: set[str] = set(EXCLUDE_PACKAGES)
|
||||
with path.joinpath("requirements.txt").open("w") as f:
|
||||
for dist in distributions():
|
||||
pkg_name = normalize_name(dist.metadata["Name"])
|
||||
if pkg_name in seen_packages:
|
||||
continue
|
||||
seen_packages.add(pkg_name)
|
||||
f.write(_get_requirement_string(dist) + "\n")
|
||||
if extras:
|
||||
f.write("\n".join(extras) + "\n")
|
||||
|
||||
|
||||
async def _write_snapshot(path: ZPath, data: dict, models: list | None = None) -> None:
|
||||
proc = await asyncio.subprocess.create_subprocess_exec(
|
||||
"git", "rev-parse", "HEAD", stdout=subprocess.PIPE, cwd=folder_paths.base_path
|
||||
def get_snapshot_path() -> Path | None:
|
||||
manager_file_path = Path(
|
||||
folder_paths.get_user_directory(), "default", "ComfyUI-Manager"
|
||||
)
|
||||
stdout, _ = await proc.communicate()
|
||||
if models is None:
|
||||
print("Package => Writing models")
|
||||
models = await _get_models()
|
||||
return manager_file_path / "snapshots"
|
||||
|
||||
|
||||
async def _save_snapshot() -> dict[str, Any]:
|
||||
save_snapshot_route = next(
|
||||
(
|
||||
route
|
||||
for route in PromptServer.instance.routes
|
||||
if route.path == "/snapshot/save"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not save_snapshot_route:
|
||||
raise RuntimeError("ComfyUI-Manager must be installed to save snapshot")
|
||||
await save_snapshot_route.handler(None)
|
||||
snapshot_path = get_snapshot_path()
|
||||
if not snapshot_path.exists():
|
||||
raise RuntimeError("Snapshot save failed")
|
||||
|
||||
most_recent = max(
|
||||
snapshot_path.glob("*.json"), key=lambda x: x.stat().st_mtime, default=None
|
||||
)
|
||||
if not most_recent:
|
||||
raise RuntimeError("Snapshot save failed")
|
||||
with most_recent.open("r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
async def _write_snapshot(path: ZPath, data: dict, models: list) -> None:
|
||||
snapshot = await _save_snapshot()
|
||||
for package in list(snapshot["pips"]):
|
||||
if any(
|
||||
fnmatch(normalize_name(package.split("==")[0]), pat)
|
||||
for pat in EXCLUDE_PACKAGES
|
||||
):
|
||||
del snapshot["pips"][package]
|
||||
with path.joinpath("snapshot.json").open("w") as f:
|
||||
data = {
|
||||
"python": f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||
"comfyui": stdout.decode().strip(),
|
||||
"models": models,
|
||||
"custom_nodes": await _get_custom_nodes(),
|
||||
}
|
||||
f.write(json.dumps(data, indent=2))
|
||||
snapshot.update(
|
||||
{
|
||||
"python": f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||
"models": models,
|
||||
}
|
||||
)
|
||||
f.write(json.dumps(snapshot, indent=2))
|
||||
|
||||
|
||||
def _is_port_in_use(port: int | str, host="localhost"):
|
||||
@@ -185,47 +184,6 @@ async def _get_models(
|
||||
return models
|
||||
|
||||
|
||||
async def _get_custom_nodes() -> list:
|
||||
print("Package => Writing custom nodes")
|
||||
custom_nodes = os.path.join(folder_paths.base_path, "custom_nodes")
|
||||
coros = []
|
||||
|
||||
async def get_node_info(subdir: Path) -> dict:
|
||||
proc = await asyncio.subprocess.create_subprocess_exec(
|
||||
"git",
|
||||
"config",
|
||||
"--get",
|
||||
"remote.origin.url",
|
||||
cwd=subdir,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
stdout, _ = await proc.communicate()
|
||||
url = stdout.decode().strip()
|
||||
|
||||
proc = await asyncio.subprocess.create_subprocess_exec(
|
||||
"git",
|
||||
"rev-parse",
|
||||
"HEAD",
|
||||
cwd=subdir,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
stdout, _ = await proc.communicate()
|
||||
commit_hash = stdout.decode().strip()
|
||||
return {
|
||||
"url": url,
|
||||
"commit_hash": commit_hash,
|
||||
"disabled": subdir.name.endswith(".disabled"),
|
||||
"path": str(subdir.relative_to(custom_nodes)),
|
||||
}
|
||||
|
||||
for subdir in Path(custom_nodes).iterdir():
|
||||
if not subdir.is_dir() or not subdir.joinpath(".git").exists():
|
||||
continue
|
||||
coros.append(get_node_info(subdir))
|
||||
|
||||
return await asyncio.gather(*coros)
|
||||
|
||||
|
||||
async def _write_workflow(path: ZPath, data: dict) -> None:
|
||||
print("Package => Writing workflow")
|
||||
with path.joinpath("workflow_api.json").open("w") as f:
|
||||
@@ -449,7 +407,7 @@ async def serve(request):
|
||||
)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/bentoml/serve/heartbeat")
|
||||
@PromptServer.instance.routes.get("/bentoml/serve/heartbeat")
|
||||
async def heartbeat(_):
|
||||
running = DevServer.feed_watch_dog()
|
||||
|
||||
@@ -475,14 +433,15 @@ async def _prepare_pack(
|
||||
working_dir: ZPath,
|
||||
data: dict,
|
||||
store_models: bool = False,
|
||||
ensure_source: bool = True,
|
||||
) -> None:
|
||||
model_filter = set(data.get("models", []))
|
||||
models = await _get_models(
|
||||
store_models=store_models,
|
||||
model_filter=model_filter,
|
||||
ensure_source=ensure_source,
|
||||
)
|
||||
|
||||
await _write_requirements(working_dir, ["comfy-cli", "fastapi", "comfy-pack"])
|
||||
await _write_snapshot(working_dir, data, models)
|
||||
await _write_workflow(working_dir, data)
|
||||
await _write_inputs(working_dir, data)
|
||||
@@ -548,7 +507,7 @@ async def build_bento_api(request):
|
||||
|
||||
with tempfile.TemporaryDirectory(suffix="-bento", prefix="comfy-pack-") as temp_dir:
|
||||
temp_dir_path = Path(temp_dir)
|
||||
await _prepare_pack(temp_dir_path, data, store_models=True)
|
||||
await _prepare_pack(temp_dir_path, data, store_models=True, ensure_source=False)
|
||||
|
||||
# create a bento
|
||||
try:
|
||||
|
||||
+191
-2
@@ -1,14 +1,19 @@
|
||||
import glob
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import sys
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
|
||||
import folder_paths
|
||||
import node_helpers
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image, ImageOps, ImageSequence
|
||||
from comfy_extras.nodes_audio import SaveAudio
|
||||
from comfy_extras.nodes_video import SaveVideo
|
||||
from PIL import Image, ImageOps, ImageSequence, PngImagePlugin
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
|
||||
from .monkeypatch import set_bentoml_output
|
||||
@@ -163,6 +168,100 @@ class OutputImage:
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
|
||||
class OutputImageWithStringTxt:
|
||||
COLOR = (142, 36, 170)
|
||||
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
self.prefix_append = ""
|
||||
self.compress_level = 4
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE", {"tooltip": "The images to save."}),
|
||||
"filename_prefix": ("STRING", {"default": "cpack_output_"}),
|
||||
"text": ("STRING", {"default": ""}),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "save_images"
|
||||
CPACK_NODE = True
|
||||
OUTPUT_NODE = True
|
||||
|
||||
CATEGORY = "ComfyPack/output"
|
||||
DESCRIPTION = (
|
||||
"Saves the input images (and optional text) to your ComfyUI output directory."
|
||||
)
|
||||
|
||||
def save_images(
|
||||
self,
|
||||
images,
|
||||
filename_prefix="cpack_output_",
|
||||
text="",
|
||||
prompt=None,
|
||||
extra_pnginfo=None,
|
||||
):
|
||||
filename_prefix += self.prefix_append
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = (
|
||||
get_save_image_path(
|
||||
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
|
||||
)
|
||||
)
|
||||
|
||||
base_counter = counter # use for name zip
|
||||
zip_filename = f"{filename}_batch_{base_counter:05}.zip"
|
||||
zip_path = os.path.join(full_output_folder, zip_filename)
|
||||
|
||||
# create ZIP file
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for batch_number, image in enumerate(images):
|
||||
# temp store img to RAM
|
||||
i = 255.0 * image.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
|
||||
# write meta data
|
||||
metadata = PngImagePlugin.PngInfo()
|
||||
if prompt is not None:
|
||||
metadata.add_text("prompt", json.dumps(prompt))
|
||||
if extra_pnginfo is not None:
|
||||
for x in extra_pnginfo:
|
||||
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
||||
|
||||
# write img file to RAM buffer
|
||||
img_buffer = BytesIO()
|
||||
img.save(
|
||||
img_buffer,
|
||||
format="PNG",
|
||||
pnginfo=metadata,
|
||||
compress_level=self.compress_level,
|
||||
)
|
||||
img_buffer.seek(0)
|
||||
|
||||
# write img into ZIP file
|
||||
image_filename = f"image_{batch_number:05}.png"
|
||||
zipf.writestr(image_filename, img_buffer.read())
|
||||
|
||||
# write txt into ZIP file
|
||||
text_filename = f"text_{batch_number:05}.txt"
|
||||
zipf.writestr(text_filename, text)
|
||||
|
||||
# return zip as output
|
||||
out = [{"filename": zip_filename, "subfolder": subfolder, "type": "zip"}]
|
||||
return {
|
||||
"ui": {
|
||||
"zip": out,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ImageInput:
|
||||
COLOR = (142, 36, 170)
|
||||
|
||||
@@ -353,14 +452,99 @@ class AnyInput:
|
||||
return True
|
||||
|
||||
|
||||
class OutputZip:
|
||||
CATEGORY = "ComfyPack/output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "null_op"
|
||||
|
||||
def null_op(self):
|
||||
return ()
|
||||
|
||||
|
||||
class OutputAudio(SaveAudio):
|
||||
CPACK_NODE = True
|
||||
CATEGORY = "ComfyPack/output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"audio": ("AUDIO",),
|
||||
"filename_prefix": ("STRING", {"default": "cpack_output_"}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
|
||||
class OutputVideo(SaveVideo):
|
||||
CPACK_NODE = True
|
||||
CATEGORY = "ComfyPack/output"
|
||||
|
||||
|
||||
class OutputTextFile:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {"forceInput": True}),
|
||||
"filename_prefix": ("STRING", {"default": "cpack_output_"}),
|
||||
},
|
||||
"optional": {
|
||||
"file_extension": ("STRING", {"default": ".txt"}),
|
||||
},
|
||||
}
|
||||
|
||||
OUTPUT_NODE = True
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "save_text_file"
|
||||
CATEGORY = "ComfyPack/output"
|
||||
CPACK_NODE = True
|
||||
|
||||
def save_text_file(
|
||||
self, text: str, filename_prefix: str, file_extension: str = ".txt"
|
||||
):
|
||||
subfolder, filename_prefix = os.path.split(os.path.normpath(filename_prefix))
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
full_output_folder = os.path.join(output_dir, subfolder)
|
||||
|
||||
full_output_filename = self.get_output_filename(
|
||||
full_output_folder, filename_prefix, file_extension
|
||||
)
|
||||
with open(full_output_filename, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(text)
|
||||
return (text, {"ui": {"string": text}})
|
||||
|
||||
@staticmethod
|
||||
def get_output_filename(folder: str, prefix: str, extension: str) -> str:
|
||||
matched_files = [
|
||||
os.path.basename(f)[len(prefix) + 1 : -len(extension)]
|
||||
for f in glob.glob(os.path.join(folder, f"{prefix}_*{extension}"))
|
||||
]
|
||||
print("MATCHING", matched_files)
|
||||
max_count = max(
|
||||
(int(name) for name in matched_files if name.isdigit()), default=0
|
||||
)
|
||||
return os.path.join(folder, f"{prefix}_{max_count + 1:04d}{extension}")
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"CPackOutputFile": OutputFile,
|
||||
"CPackOutputImage": OutputImage,
|
||||
"CPackOutputAudio": OutputAudio,
|
||||
"CPackOutputVideo": OutputVideo,
|
||||
"CPackOutputZip": OutputImageWithStringTxt,
|
||||
"CPackOutputZipSwitch": OutputZip,
|
||||
"CPackInputImage": ImageInput,
|
||||
"CPackInputString": StringInput,
|
||||
"CPackInputInt": IntInput,
|
||||
"CPackInputFile": FileInput,
|
||||
"CPackInputAny": AnyInput,
|
||||
"CPackOutputTextFile": OutputTextFile,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -370,5 +554,10 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"CPackInputFile": "File Input",
|
||||
"CPackInputAny": "Any Input",
|
||||
"CPackOutputImage": "Image Output",
|
||||
"CPackOutputAudio": "Audio Output",
|
||||
"CPackOutputVideo": "Video Output",
|
||||
"CPackOutputFile": "File Output",
|
||||
"CPackOutputZip": "Zip Output(img + txt file)",
|
||||
"CPackOutputZipSwitch": "Enable Zip Output",
|
||||
"CPackOutputTextFile": "Output Text to File",
|
||||
}
|
||||
|
||||
+106
-98
@@ -5,8 +5,8 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import click
|
||||
|
||||
@@ -31,6 +31,7 @@ def _ensure_uv() -> None:
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option()
|
||||
def main():
|
||||
"""comfy-pack CLI"""
|
||||
pass
|
||||
@@ -56,27 +57,25 @@ def main():
|
||||
def init(dir: str, verbose: int):
|
||||
import os
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
import rich
|
||||
|
||||
# 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]")
|
||||
rich.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(
|
||||
rich.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]")
|
||||
rich.print(f"[red]Error: Invalid directory path - {str(e)}[/red]")
|
||||
return 1
|
||||
|
||||
# Check git installation
|
||||
@@ -87,7 +86,7 @@ def init(dir: str, verbose: int):
|
||||
capture_output=True,
|
||||
)
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
console.print("[red]Error: git is not installed or not in PATH[/red]")
|
||||
rich.print("[red]Error: git is not installed or not in PATH[/red]")
|
||||
return 1
|
||||
|
||||
# Check if we have write permissions
|
||||
@@ -98,36 +97,36 @@ def init(dir: str, verbose: int):
|
||||
test_file.touch()
|
||||
test_file.unlink()
|
||||
except (OSError, PermissionError) as e:
|
||||
console.print(f"[red]Error: No write permission in {dir} - {str(e)}[/red]")
|
||||
rich.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]")
|
||||
rich.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]")
|
||||
rich.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(
|
||||
rich.print(
|
||||
"[yellow]Warning: Less than 2GB free disk space available[/yellow]"
|
||||
)
|
||||
except Exception as e:
|
||||
console.print(
|
||||
rich.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]")
|
||||
rich.print("[green]Cloning ComfyUI...[/green]")
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
@@ -139,7 +138,7 @@ def init(dir: str, verbose: int):
|
||||
)
|
||||
|
||||
# Update ComfyUI
|
||||
console.print("[green]Updating ComfyUI...[/green]")
|
||||
rich.print("[green]Updating ComfyUI...[/green]")
|
||||
subprocess.run(
|
||||
["git", "pull"],
|
||||
cwd=install_dir,
|
||||
@@ -148,7 +147,7 @@ def init(dir: str, verbose: int):
|
||||
|
||||
# Create and activate venv
|
||||
venv_dir = install_dir / ".venv"
|
||||
console.print("[green]Creating virtual environment with uv...[/green]")
|
||||
rich.print("[green]Creating virtual environment with uv...[/green]")
|
||||
if venv_dir.exists():
|
||||
shutil.rmtree(venv_dir)
|
||||
subprocess.run(
|
||||
@@ -164,7 +163,7 @@ def init(dir: str, verbose: int):
|
||||
python = str(venv_dir / "bin" / "python")
|
||||
|
||||
# Install requirements with uv
|
||||
console.print("[green]Installing ComfyUI requirements with uv...[/green]")
|
||||
rich.print("[green]Installing ComfyUI requirements with uv...[/green]")
|
||||
subprocess.run(
|
||||
["uv", "pip", "install", "pip", "--upgrade"],
|
||||
env={
|
||||
@@ -183,7 +182,7 @@ def init(dir: str, verbose: int):
|
||||
)
|
||||
|
||||
# Install comfy-pack as custom node
|
||||
console.print("[green]Installing comfy-pack custom nodes...[/green]")
|
||||
rich.print("[green]Installing comfy-pack custom nodes...[/green]")
|
||||
custom_nodes_dir = install_dir / "custom_nodes"
|
||||
custom_nodes_dir.mkdir(exist_ok=True)
|
||||
|
||||
@@ -225,12 +224,12 @@ def init(dir: str, verbose: int):
|
||||
)
|
||||
|
||||
version = get_self_git_commit() or "unknown"
|
||||
console.print(
|
||||
rich.print(
|
||||
f"\n[green]✓ Installation completed! (comfy-pack version: {version})[/green]"
|
||||
)
|
||||
console.print(f"ComfyUI directory: {install_dir}")
|
||||
rich.print(f"ComfyUI directory: {install_dir}")
|
||||
|
||||
console.print(
|
||||
rich.print(
|
||||
"\n[green]Next steps:[/green]\n"
|
||||
f"1. cd {dir}\n"
|
||||
"2. source .venv/bin/activate # On Windows: .venv\\Scripts\\activate\n"
|
||||
@@ -242,7 +241,7 @@ def init(dir: str, verbose: int):
|
||||
name="unpack",
|
||||
help="Restore the ComfyUI workspace to specified directory",
|
||||
)
|
||||
@click.argument("cpack", type=click.Path(exists=True, dir_okay=False))
|
||||
@click.argument("cpack", type=click.Path(exists=True))
|
||||
@click.option(
|
||||
"--dir",
|
||||
"-d",
|
||||
@@ -256,33 +255,69 @@ def init(dir: str, verbose: int):
|
||||
type=click.BOOL,
|
||||
is_flag=True,
|
||||
)
|
||||
@click.option(
|
||||
"--no-models",
|
||||
default=False,
|
||||
type=click.BOOL,
|
||||
is_flag=True,
|
||||
help="Do not install models",
|
||||
)
|
||||
@click.option(
|
||||
"--no-venv",
|
||||
is_flag=True,
|
||||
help="Do not create a virtual environment for ComfyUI",
|
||||
default=False,
|
||||
)
|
||||
@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 rich.console import Console
|
||||
@click.option(
|
||||
"--preheat",
|
||||
is_flag=True,
|
||||
help="Preheat the workspace after unpacking",
|
||||
default=False,
|
||||
)
|
||||
def unpack_cmd(
|
||||
cpack: str,
|
||||
dir: str,
|
||||
include_disabled_models: bool,
|
||||
no_models: bool,
|
||||
no_venv: bool,
|
||||
verbose: int,
|
||||
preheat: bool,
|
||||
):
|
||||
import rich
|
||||
|
||||
from .package import install
|
||||
|
||||
console = Console()
|
||||
|
||||
install(cpack, dir, verbose=verbose, all_models=include_disabled_models)
|
||||
console.print("\n[green]✓ ComfyUI Workspace is restored![/green]")
|
||||
console.print(f"{dir}")
|
||||
|
||||
console.print(
|
||||
"\n[green] Next steps: [/green]\n"
|
||||
"1. Change directory to the restored workspace\n"
|
||||
"2. Source the virtual environment by running `source .venv/bin/activate`\n"
|
||||
"3. Run the ComfyUI project by running `python main.py`"
|
||||
install(
|
||||
cpack,
|
||||
dir,
|
||||
verbose=verbose,
|
||||
all_models=include_disabled_models,
|
||||
prepare_models=not no_models,
|
||||
no_venv=no_venv,
|
||||
preheat=preheat,
|
||||
)
|
||||
rich.print("\n[green]✓ ComfyUI Workspace is restored at:[/green]")
|
||||
rich.print(os.path.abspath(dir))
|
||||
steps = [f"Change directory to the restored workspace: `cd {dir}`"]
|
||||
if not no_venv:
|
||||
steps.append(
|
||||
"Source the virtual environment by running `source .venv/bin/activate`"
|
||||
)
|
||||
steps.append("Run the ComfyUI project by running `python main.py`")
|
||||
|
||||
rich.print(f"\n[green]⏭️ Next steps: [/green]\n1. {steps[0]}\n2. {steps[1]}")
|
||||
if len(steps) > 2:
|
||||
rich.print(f"3. {steps[2]}")
|
||||
|
||||
|
||||
def _print_schema(schema, verbose: int = 0):
|
||||
from rich.console import Console
|
||||
import rich
|
||||
from rich.table import Table
|
||||
|
||||
table = Table(title="")
|
||||
@@ -313,7 +348,7 @@ def _print_schema(schema, verbose: int = 0):
|
||||
range_str,
|
||||
)
|
||||
|
||||
Console().print(table)
|
||||
rich.print(table)
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
@@ -341,8 +376,8 @@ def _get_cache_workspace(cpack: str):
|
||||
)
|
||||
@click.pass_context
|
||||
def run(ctx, cpack: str, output_dir: str, help: bool, verbose: int):
|
||||
import rich
|
||||
from pydantic import ValidationError
|
||||
from rich.console import Console
|
||||
|
||||
from .utils import generate_input_model
|
||||
|
||||
@@ -350,35 +385,31 @@ def run(ctx, cpack: str, output_dir: str, help: bool, verbose: int):
|
||||
zip([k.lstrip("-").replace("-", "_") for k in ctx.args[::2]], ctx.args[1::2])
|
||||
)
|
||||
|
||||
console = Console()
|
||||
|
||||
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())
|
||||
with zipfile.ZipFile(cpack) as z:
|
||||
workflow = json.loads(z.read("workflow_api.json"))
|
||||
|
||||
input_model = generate_input_model(workflow)
|
||||
|
||||
# If help is requested, show command help and input schema
|
||||
if help:
|
||||
console.print(
|
||||
rich.print(
|
||||
'Usage: comfy-pack run [OPTIONS] CPACK --input1 "value1" --input2 "value2" ...'
|
||||
)
|
||||
console.print("Run a ComfyUI package with the given inputs:")
|
||||
rich.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]")
|
||||
rich.print("[green]✓ Input is valid![/green]")
|
||||
for field, value in validated_data.model_dump().items():
|
||||
console.print(f"{field}: {value}")
|
||||
rich.print(f"{field}: {value}")
|
||||
except ValidationError as e:
|
||||
console.print("[red]✗ Validation failed![/red]")
|
||||
rich.print("[red]✗ Validation failed![/red]")
|
||||
for error in e.errors():
|
||||
console.print(f"- {error['loc'][0]}: {error['msg']}")
|
||||
rich.print(f"- {error['loc'][0]}: {error['msg']}")
|
||||
|
||||
console.print("\n[yellow]Expected inputs:[/yellow]")
|
||||
rich.print("\n[yellow]Expected inputs:[/yellow]")
|
||||
_print_schema(input_model.model_json_schema(), verbose)
|
||||
return 1
|
||||
|
||||
@@ -386,19 +417,19 @@ def run(ctx, cpack: str, output_dir: str, help: bool, verbose: int):
|
||||
|
||||
workspace = _get_cache_workspace(cpack)
|
||||
if not (workspace / "DONE").exists():
|
||||
console.print("\n[green]✓ Restoring ComfyUI Workspace...[/green]")
|
||||
rich.print("\n[green]✓ Restoring ComfyUI Workspace...[/green]")
|
||||
if workspace.exists():
|
||||
shutil.rmtree(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]")
|
||||
console.print(f"{workspace}")
|
||||
rich.print("\n[green]✓ ComfyUI Workspace is restored![/green]")
|
||||
rich.print(f"{workspace}")
|
||||
|
||||
from .run import ComfyUIServer, run_workflow
|
||||
|
||||
with ComfyUIServer(str(workspace.absolute()), verbose=verbose) as server:
|
||||
console.print("\n[green]✓ ComfyUI is launched in the background![/green]")
|
||||
rich.print("\n[green]✓ ComfyUI is launched in the background![/green]")
|
||||
results = run_workflow(
|
||||
server.host,
|
||||
server.port,
|
||||
@@ -408,17 +439,17 @@ def run(ctx, cpack: str, output_dir: str, help: bool, verbose: int):
|
||||
workspace=server.workspace,
|
||||
**validated_data.model_dump(),
|
||||
)
|
||||
console.print("\n[green]✓ Workflow is executed successfully![/green]")
|
||||
rich.print("\n[green]✓ Workflow is executed successfully![/green]")
|
||||
if results:
|
||||
console.print("\n[green]✓ Retrieved outputs:[/green]")
|
||||
rich.print("\n[green]✓ Retrieved outputs:[/green]")
|
||||
if isinstance(results, dict):
|
||||
for field, value in results.items():
|
||||
console.print(f"{field}: {value}")
|
||||
rich.print(f"{field}: {value}")
|
||||
elif isinstance(results, list):
|
||||
for i, value in enumerate(results):
|
||||
console.print(f"{i}: {value}")
|
||||
rich.print(f"{i}: {value}")
|
||||
else:
|
||||
console.print(results)
|
||||
rich.print(results)
|
||||
|
||||
|
||||
@main.command(name="build-bento")
|
||||
@@ -480,7 +511,7 @@ def setup_cloud_client(
|
||||
@click.option(
|
||||
"-w",
|
||||
"--workspace",
|
||||
type=click.Path(file_okay=False),
|
||||
type=click.Path(file_okay=False, path_type=Path),
|
||||
default="workspace",
|
||||
help="Workspace directory, defaults to './workspace'.",
|
||||
)
|
||||
@@ -491,53 +522,30 @@ def setup_cloud_client(
|
||||
expose_value=False,
|
||||
callback=setup_cloud_client,
|
||||
)
|
||||
def unpack_bento(bento: str, workspace: str, verbose: int):
|
||||
def unpack_bento(bento: str, workspace: Path, verbose: int):
|
||||
"""Restore the ComfyUI workspace from a given bento."""
|
||||
import bentoml
|
||||
|
||||
from .package import install_comfyui, install_custom_modules, install_dependencies
|
||||
from .package import install
|
||||
|
||||
bento_obj = bentoml.get(bento)
|
||||
comfy_workspace = Path(workspace)
|
||||
comfy_workspace.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not comfy_workspace.joinpath(".DONE").exists():
|
||||
for model in bento_obj.info.models:
|
||||
model.to_model().resolve()
|
||||
snapshot = json.loads(Path(bento_obj.path_of("src/snapshot.json")).read_text())
|
||||
install_comfyui(snapshot, comfy_workspace, verbose=verbose)
|
||||
reqs_txt = bento_obj.path_of("env/python/requirements.txt")
|
||||
install_dependencies(snapshot, reqs_txt, comfy_workspace, verbose=verbose)
|
||||
try:
|
||||
bento_obj = bentoml.get(bento)
|
||||
except bentoml.exceptions.NotFound:
|
||||
click.echo(
|
||||
f"Bento {bento} not found in the local repository, trying to pull from BentoCloud",
|
||||
err=True,
|
||||
)
|
||||
bentoml.pull(bento)
|
||||
bento_obj = bentoml.get(bento)
|
||||
|
||||
for f in Path(bento_obj.path_of("src/input")).glob("*"):
|
||||
if f.is_file():
|
||||
shutil.copy(f, comfy_workspace / "input" / f.name)
|
||||
elif f.is_dir():
|
||||
shutil.copytree(
|
||||
f, comfy_workspace / "input" / f.name, dirs_exist_ok=True
|
||||
)
|
||||
install_custom_modules(snapshot, comfy_workspace, verbose=verbose)
|
||||
for model in snapshot["models"]:
|
||||
if model.get("disabled", False):
|
||||
continue
|
||||
model_path = comfy_workspace / cast(str, model["filename"])
|
||||
if model_path.exists():
|
||||
continue
|
||||
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")
|
||||
click.echo(f"Copying {model_file} to {model_path}")
|
||||
model_path.symlink_to(model_file)
|
||||
else:
|
||||
click.echo("WARN: Unrecognized model source, the model may be missing")
|
||||
comfy_workspace.joinpath(".DONE").touch()
|
||||
install(bento_obj.path_of("src"), workspace, verbose=verbose, prepare_models=False)
|
||||
|
||||
if os.name == "nt":
|
||||
exe = "Scripts/python.exe"
|
||||
else:
|
||||
exe = "bin/python"
|
||||
click.echo(
|
||||
f"Workspace is ready at {comfy_workspace}\n"
|
||||
f"You can start ComfyUI by running `cd {comfy_workspace} && .venv/{exe} main.py`",
|
||||
f"Workspace is ready at {workspace}\n"
|
||||
f"You can start ComfyUI by running `cd {workspace} && .venv/{exe} main.py`",
|
||||
color="green",
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from .const import MODEL_SOURCE_CACHE_FILE
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
|
||||
from .const import MODEL_SOURCE_CACHE_FILE
|
||||
|
||||
# MODEL_NAME = r"[a-zA-Z0-9-._]+"
|
||||
# COMMIT = r"[a-f0-9]+"
|
||||
|
||||
@@ -14,9 +15,8 @@ PATH_PATTERN = re.compile(
|
||||
|
||||
|
||||
async def _lookup_huggingface_model(model_sha: str) -> dict:
|
||||
from duckduckgo_search import DDGS
|
||||
import aiohttp
|
||||
|
||||
from duckduckgo_search import DDGS
|
||||
query = f"site:huggingface.co blob {model_sha}"
|
||||
|
||||
try:
|
||||
@@ -25,7 +25,7 @@ async def _lookup_huggingface_model(model_sha: str) -> dict:
|
||||
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
for result in search_results:
|
||||
url = result['link']
|
||||
url = result['href']
|
||||
if "blob" not in url:
|
||||
continue
|
||||
|
||||
@@ -38,11 +38,9 @@ async def _lookup_huggingface_model(model_sha: str) -> dict:
|
||||
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,
|
||||
"download_url": path,
|
||||
"url": path,
|
||||
"repo": repo,
|
||||
"commit": commit,
|
||||
"path": path,
|
||||
|
||||
+126
-60
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -66,6 +67,18 @@ def install_comfyui(snapshot, workspace: Path, verbose: int = 0):
|
||||
return
|
||||
shutil.rmtree(workspace)
|
||||
_clone_commit(COMFYUI_REPO, comfyui_commit, workspace, verbose=verbose)
|
||||
manager_node = next(
|
||||
url for url in snapshot.get("git_custom_nodes", {}) if "ComfyUI-Manager" in url
|
||||
)
|
||||
if manager_node:
|
||||
manager_commit = snapshot["git_custom_nodes"][manager_node]["hash"].strip()
|
||||
print("Installing ComfyUI-Manager")
|
||||
_clone_commit(
|
||||
manager_node,
|
||||
manager_commit,
|
||||
workspace / "custom_nodes" / "ComfyUI-Manager",
|
||||
verbose=verbose,
|
||||
)
|
||||
with open(workspace / ".DONE", "w") as f:
|
||||
f.write(comfyui_commit)
|
||||
|
||||
@@ -88,6 +101,7 @@ def install_custom_modules(snapshot, workspace: Path, verbose: int = 0):
|
||||
continue
|
||||
shutil.rmtree(module_dir)
|
||||
|
||||
print(f"Installing custom node {url}")
|
||||
commit_hash = module["commit_hash"]
|
||||
_clone_commit(url, commit_hash, module_dir, verbose=verbose)
|
||||
|
||||
@@ -122,33 +136,39 @@ def install_custom_modules(snapshot, workspace: Path, verbose: int = 0):
|
||||
|
||||
|
||||
def install_dependencies(
|
||||
snapshot: dict,
|
||||
req_file: str,
|
||||
python_version: str,
|
||||
req_files: list[str],
|
||||
workspace: Path,
|
||||
verbose: int = 0,
|
||||
):
|
||||
no_deps: bool = False,
|
||||
no_venv: bool = False,
|
||||
) -> Path:
|
||||
print("Installing Python dependencies")
|
||||
python_version = snapshot["python"]
|
||||
stdout = None if verbose > 0 else subprocess.DEVNULL
|
||||
stderr = None if verbose > 1 else subprocess.DEVNULL
|
||||
|
||||
venv = workspace / ".venv"
|
||||
if (venv / "DONE").exists():
|
||||
return
|
||||
venv_py = (
|
||||
venv / "Scripts" / "python.exe" if os.name == "nt" else venv / "bin" / "python"
|
||||
)
|
||||
subprocess.check_call(
|
||||
[
|
||||
"uv",
|
||||
"venv",
|
||||
"--python",
|
||||
python_version,
|
||||
venv,
|
||||
],
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
if no_venv:
|
||||
print("Using the current Python environment")
|
||||
venv_py = Path(sys.executable)
|
||||
else:
|
||||
venv = workspace / ".venv"
|
||||
venv_py = (
|
||||
venv / "Scripts" / "python.exe"
|
||||
if os.name == "nt"
|
||||
else venv / "bin" / "python"
|
||||
)
|
||||
if (venv / "DONE").exists():
|
||||
return venv_py
|
||||
subprocess.check_call(
|
||||
[
|
||||
"uv",
|
||||
"venv",
|
||||
"--python",
|
||||
python_version,
|
||||
venv,
|
||||
],
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
subprocess.check_call(
|
||||
[
|
||||
"uv",
|
||||
@@ -162,28 +182,28 @@ def install_dependencies(
|
||||
stderr=stderr,
|
||||
)
|
||||
if verbose > 0:
|
||||
print(f"Installing dependencies from {req_file}")
|
||||
print(f"Installing dependencies from {req_files}")
|
||||
install_cmd = [
|
||||
"uv",
|
||||
"pip",
|
||||
"install",
|
||||
"-p",
|
||||
str(venv_py),
|
||||
"-r",
|
||||
req_file,
|
||||
"--no-deps",
|
||||
]
|
||||
if STRICT_MODE:
|
||||
pass
|
||||
else:
|
||||
for req_file in req_files:
|
||||
install_cmd.extend(["-r", str(req_file)])
|
||||
if not STRICT_MODE:
|
||||
install_cmd.extend(["--index-strategy", "unsafe-best-match"])
|
||||
if no_deps:
|
||||
install_cmd.append("--no-deps")
|
||||
subprocess.check_call(
|
||||
install_cmd,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
with open(venv / "DONE", "w") as f:
|
||||
f.write("DONE")
|
||||
if not no_venv:
|
||||
with open(venv / "DONE", "w") as f:
|
||||
f.write("DONE")
|
||||
return venv_py
|
||||
|
||||
|
||||
@@ -198,13 +218,23 @@ def get_search_url(sha: str) -> str:
|
||||
|
||||
def download_file(url: str, dest_path: Path, progress_callback=None):
|
||||
"""Download file with progress tracking"""
|
||||
if subprocess.call(["curl", "--version"], stdout=subprocess.DEVNULL) == 0:
|
||||
subprocess.check_call(
|
||||
["curl", "-L", url, "-o", str(dest_path)],
|
||||
)
|
||||
return True
|
||||
|
||||
# prepare auth token from huggingface if possible
|
||||
if (token := os.getenv("HF_TOKEN")) and ("huggingface" in url):
|
||||
bearer = f"Bearer {token}"
|
||||
urllib_request = urllib.request.Request(url, headers={"Authorization": bearer})
|
||||
curl_auth = ["-H", f"Authorization: {bearer}"]
|
||||
else:
|
||||
curl_auth = []
|
||||
urllib_request = url
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(url) as response:
|
||||
if shutil.which("curl"):
|
||||
subprocess.check_call(
|
||||
["curl", "-L", url, *curl_auth, "--fail", "-o", str(dest_path)],
|
||||
)
|
||||
return True
|
||||
with urllib.request.urlopen(urllib_request) as response:
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
block_size = 8192
|
||||
downloaded = 0
|
||||
@@ -381,49 +411,82 @@ def retrieve_models(
|
||||
def install(
|
||||
cpack: str | Path,
|
||||
workspace: str | Path = "workspace",
|
||||
preheat: bool = True,
|
||||
preheat: bool = False,
|
||||
prepare_models: bool = True,
|
||||
all_models: bool = False,
|
||||
no_venv: bool = False,
|
||||
verbose: int = 0,
|
||||
):
|
||||
workspace = Path(workspace)
|
||||
cpack = Path(cpack)
|
||||
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)
|
||||
with contextlib.ExitStack() as stack:
|
||||
if cpack.is_file():
|
||||
temp_dir = stack.enter_context(tempfile.TemporaryDirectory())
|
||||
pack_dir = Path(temp_dir) / ".cpack"
|
||||
shutil.unpack_archive(cpack, pack_dir)
|
||||
else:
|
||||
pack_dir = cpack
|
||||
snapshot = json.loads((pack_dir / "snapshot.json").read_text())
|
||||
req_txt_file = pack_dir / "requirements.txt"
|
||||
if "pips" not in snapshot:
|
||||
raise RuntimeError(
|
||||
"This cpack is generated by an old version of comfy-pack, "
|
||||
"please use comfy-pack<0.4.0 to unpack it."
|
||||
)
|
||||
|
||||
install_comfyui(snapshot, workspace, verbose=verbose)
|
||||
install_dependencies(snapshot, str(req_txt_file), workspace, verbose=verbose)
|
||||
py = install_dependencies(
|
||||
snapshot["python"],
|
||||
[
|
||||
str(workspace / "requirements.txt"),
|
||||
str(
|
||||
workspace / "custom_nodes" / "ComfyUI-Manager" / "requirements.txt"
|
||||
),
|
||||
],
|
||||
workspace,
|
||||
no_venv=no_venv,
|
||||
verbose=verbose,
|
||||
)
|
||||
cm_cli = Path("custom_nodes", "ComfyUI-Manager", "cm-cli.py")
|
||||
subprocess.check_call(
|
||||
[
|
||||
str(py),
|
||||
str(cm_cli),
|
||||
"restore-snapshot",
|
||||
"--pip-non-url",
|
||||
"--pip-non-local-url",
|
||||
str(pack_dir / "snapshot.json"),
|
||||
],
|
||||
cwd=workspace,
|
||||
)
|
||||
|
||||
for f in (pack_dir / "input").glob("*"):
|
||||
if f.is_file():
|
||||
shutil.copy(f, workspace / "input" / f.name)
|
||||
elif f.is_dir():
|
||||
shutil.copytree(f, workspace / "input" / f.name, dirs_exist_ok=True)
|
||||
if prepare_models:
|
||||
retrieve_models(
|
||||
snapshot,
|
||||
workspace,
|
||||
verbose=verbose,
|
||||
download=False,
|
||||
)
|
||||
|
||||
retrieve_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,
|
||||
venv=str(workspace / ".venv"),
|
||||
venv=str(workspace / ".venv") if not no_venv else None,
|
||||
) as _:
|
||||
pass
|
||||
|
||||
retrieve_models(snapshot, workspace, verbose=verbose, all_models=all_models)
|
||||
if prepare_models:
|
||||
retrieve_models(snapshot, workspace, verbose=verbose, all_models=all_models)
|
||||
|
||||
|
||||
required_files = ["snapshot.json", "requirements.txt"]
|
||||
required_files = ["snapshot.json"]
|
||||
|
||||
|
||||
def build_bento(
|
||||
@@ -456,9 +519,9 @@ def build_bento(
|
||||
|
||||
shutil.copy2(Path(__file__).with_name("service.py"), source_dir / "service.py")
|
||||
snapshot_text = (source_dir / "snapshot.json").read_text()
|
||||
setup_script = source_dir / "setup_workspace.py"
|
||||
with Path(__file__).with_name("setup_workspace.py").open() as f:
|
||||
setup_script.write_text(f.read().format(snapshot=snapshot_text))
|
||||
setup_script = source_dir / "setup_workspace.sh"
|
||||
with Path(__file__).with_name("setup_workspace.sh").open() as f:
|
||||
setup_script.write_text(f.read().replace("<SNAPSHOT>", snapshot_text))
|
||||
# Make setup script executable in a cross-platform way
|
||||
if os.name in ("posix", "mac"):
|
||||
setup_script.chmod(setup_script.stat().st_mode | 0o755)
|
||||
@@ -477,7 +540,10 @@ def build_bento(
|
||||
docker={
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||
"system_packages": system_packages,
|
||||
"setup_script": source_dir.joinpath("setup_workspace.py").as_posix(),
|
||||
"setup_script": source_dir.joinpath("setup_workspace.sh").as_posix(),
|
||||
},
|
||||
python={
|
||||
"lock_packages": True,
|
||||
"packages": ["fastapi", "comfy-pack>=0.4.0a0"],
|
||||
},
|
||||
python={"requirements_txt": "requirements.txt", "lock_packages": True},
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ class ComfyUIServer:
|
||||
self.port = 8188
|
||||
else:
|
||||
self.port = port
|
||||
self.venv = venv
|
||||
self.venv = os.path.abspath(venv) if venv else None
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# ruff: noqa: E402
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
virtualenv = os.environ.get("VIRTUAL_ENV")
|
||||
if virtualenv and "--reload" not in sys.argv:
|
||||
print("Re-executing in virtualenv:", virtualenv)
|
||||
venv_python = os.path.join(virtualenv, "bin/python3")
|
||||
os.execl(venv_python, venv_python, *sys.argv, "--reload")
|
||||
|
||||
SNAPSHOT = """\
|
||||
{snapshot}
|
||||
"""
|
||||
|
||||
|
||||
def _get_workspace() -> tuple[Path, dict]:
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
bentoml_home = os.getenv(
|
||||
"BENTOML_HOME", os.path.join(os.path.expanduser("~"), "bentoml")
|
||||
)
|
||||
|
||||
checksum = hashlib.md5(SNAPSHOT.strip().encode("utf8")).hexdigest()
|
||||
wp = Path(bentoml_home) / "run" / "comfy_workspace" / checksum
|
||||
wp.parent.mkdir(parents=True, exist_ok=True)
|
||||
return wp, json.loads(SNAPSHOT)
|
||||
|
||||
|
||||
def prepare_comfy_workspace():
|
||||
import shutil
|
||||
|
||||
from comfy_pack.package import install_comfyui, install_custom_modules
|
||||
|
||||
verbose = 2
|
||||
comfy_workspace, snapshot = _get_workspace()
|
||||
|
||||
if not comfy_workspace.joinpath(".DONE").exists():
|
||||
if comfy_workspace.exists():
|
||||
print("Removing existing workspace")
|
||||
shutil.rmtree(comfy_workspace, ignore_errors=True)
|
||||
install_comfyui(snapshot, comfy_workspace, verbose=verbose)
|
||||
|
||||
install_custom_modules(snapshot, comfy_workspace, verbose=verbose)
|
||||
comfy_workspace.joinpath(".DONE").touch()
|
||||
subprocess.run(
|
||||
["chown", "-R", "bentoml:bentoml", str(comfy_workspace)], check=True
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
prepare_comfy_workspace()
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
CPACK=/tmp/bento-cpack
|
||||
mkdir -p "$CPACK"
|
||||
|
||||
cat <<EOF | head -c -1 > "$CPACK"/snapshot.json
|
||||
<SNAPSHOT>
|
||||
EOF
|
||||
|
||||
checksum=$(md5sum "$CPACK"/snapshot.json | awk '{print $1}')
|
||||
workspace="${BENTOML_HOME:-$HOME/bentoml}/run/comfy_workspace/${checksum}"
|
||||
|
||||
if [ -n "$VIRTUAL_ENV" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$VIRTUAL_ENV"/bin/activate
|
||||
fi
|
||||
|
||||
set -x
|
||||
comfy-pack unpack "$CPACK" -d "$workspace" --no-models --no-venv -v
|
||||
chown -R bentoml:bentoml "$workspace"
|
||||
set +x
|
||||
+39
-20
@@ -1,18 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic import BaseModel
|
||||
|
||||
CPACK_OUTPUT_NODES = {
|
||||
"CPackOutputFile",
|
||||
"CPackOutputImage",
|
||||
}
|
||||
|
||||
CPACK_PATH_INPUT_NODES = {
|
||||
"CPackInputFile",
|
||||
@@ -210,7 +207,7 @@ def populate_workflow(
|
||||
|
||||
for _, node in output_spec.items():
|
||||
node_id = node["id"]
|
||||
if node["class_type"] in CPACK_OUTPUT_NODES:
|
||||
if node["class_type"].startswith("CPackOutput"):
|
||||
workflow[node_id]["inputs"]["filename_prefix"] = (
|
||||
output_path / f"{session_id}{node_id}_"
|
||||
).as_posix()
|
||||
@@ -221,7 +218,7 @@ def retrieve_workflow_outputs(
|
||||
workflow: dict,
|
||||
output_path: Path,
|
||||
session_id: str = "",
|
||||
) -> Union[Path, list[Path], dict[str, Path], dict[str, list[Path]]]:
|
||||
) -> Union[Path, list[Path], dict[str, Path | list[Path]]]:
|
||||
"""
|
||||
Gets the output file(s) from the workflow.
|
||||
|
||||
@@ -230,7 +227,7 @@ def retrieve_workflow_outputs(
|
||||
output_path (Path): The path where output files are saved.
|
||||
|
||||
Returns:
|
||||
Union[Path, list[Path], dict[str, Path], dict[str, list[Path]]]:
|
||||
Union[Path, list[Path], dict[str, Path | list[Path]]]:
|
||||
- A single Path if there's only one output file.
|
||||
- A list of Paths if there are multiple files for a single output.
|
||||
- A dictionary mapping output names to Paths or lists of Paths for multiple outputs.
|
||||
@@ -239,26 +236,48 @@ def retrieve_workflow_outputs(
|
||||
ValueError: If the output node is not of the expected type.
|
||||
"""
|
||||
_, outputs = _parse_workflow(workflow)
|
||||
should_zip = any(
|
||||
node["class_type"] == "CPackOutputZipSwitch" for node in workflow.values()
|
||||
)
|
||||
print("Should create zip:", should_zip, outputs, workflow)
|
||||
zip_paths: list[tuple[Path, str]] = []
|
||||
if len(outputs) != 1:
|
||||
value_map = {}
|
||||
for k, node in outputs.items():
|
||||
node_id = node["id"]
|
||||
path_strs = list(output_path.glob(f"{node_id}_*"))
|
||||
path_strs = list(output_path.glob(f"{session_id}{node_id}_*"))
|
||||
zip_paths.extend(
|
||||
(p, p.name.replace(f"{session_id}{node_id}", k)) for p in path_strs
|
||||
)
|
||||
if len(path_strs) == 1:
|
||||
value_map[k] = path_strs[0]
|
||||
else:
|
||||
value_map[k] = path_strs
|
||||
return value_map
|
||||
if not should_zip:
|
||||
return value_map
|
||||
else:
|
||||
name, node = next(iter(outputs.items()))
|
||||
if not node["class_type"].startswith("CPackOutput"):
|
||||
raise ValueError(f"Node {name} is not a comfy-pack output node")
|
||||
node_id = node["id"]
|
||||
|
||||
name, node = next(iter(outputs.items()))
|
||||
if node["class_type"] not in CPACK_OUTPUT_NODES:
|
||||
raise ValueError(f"Output node {name} is not of type {CPACK_OUTPUT_NODES}")
|
||||
node_id = node["id"]
|
||||
|
||||
outs = list(output_path.glob(f"{session_id}{node_id}_*"))
|
||||
if len(outs) == 1:
|
||||
return outs[0]
|
||||
return outs
|
||||
outs = list(output_path.glob(f"{session_id}{node_id}_*"))
|
||||
zip_paths.extend(
|
||||
(p, p.name.replace(f"{session_id}{node_id}", name)) for p in outs
|
||||
)
|
||||
if not should_zip:
|
||||
if len(outs) == 1:
|
||||
return outs[0]
|
||||
return outs
|
||||
if len(zip_paths) == 1:
|
||||
return zip_paths[0][0]
|
||||
# Make a zipball from the collected files
|
||||
output_zip = output_path / f"{session_id}_output.zip"
|
||||
print(f"Creating zip file: {output_zip}")
|
||||
with zipfile.ZipFile(output_zip, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for path, name in zip_paths:
|
||||
zipf.write(path, arcname=name)
|
||||
return output_zip
|
||||
|
||||
|
||||
def get_self_git_commit() -> str | None:
|
||||
|
||||
+38
-38
@@ -247,7 +247,7 @@ class FileTreeList {
|
||||
try {
|
||||
const { workflow, output: workflow_api } = await app.graphToPrompt();
|
||||
const files = await this.getInputFiles(workflow, workflow_api);
|
||||
|
||||
|
||||
// 预先将默认选中的文件添加到选中列表
|
||||
files.forEach(file => {
|
||||
const path = file.path || file;
|
||||
@@ -255,14 +255,14 @@ class FileTreeList {
|
||||
this.state.toggle(path, true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.renderTree(this.buildTree(files));
|
||||
|
||||
|
||||
// 更新所有父目录的状态
|
||||
this.container.querySelectorAll("[data-action='check-dir']").forEach(checkbox => {
|
||||
this.updateFolderState(checkbox);
|
||||
});
|
||||
|
||||
|
||||
// 更新总数
|
||||
this.updateCount();
|
||||
} catch(e) {
|
||||
@@ -282,12 +282,12 @@ class FileTreeList {
|
||||
|
||||
buildTree(files) {
|
||||
const root = { name: 'root', children: {}, files: [] };
|
||||
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = file.path || file;
|
||||
const parts = filePath.split(/[\/\\]/);
|
||||
let current = root;
|
||||
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (i === parts.length - 1) {
|
||||
@@ -313,24 +313,24 @@ class FileTreeList {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 对目录按名称排序
|
||||
for (const dir in root.children) {
|
||||
root.children[dir].files.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
renderTree(node, level = 0) {
|
||||
const dirs = Object.values(node.children);
|
||||
const hasChildren = dirs.length > 0 || node.files.length > 0;
|
||||
|
||||
|
||||
this.container.innerHTML = this.renderNode(node, true);
|
||||
|
||||
|
||||
this.setupEventListeners();
|
||||
this.updateCount();
|
||||
|
||||
|
||||
}
|
||||
|
||||
renderNode(node, isRoot = false) {
|
||||
@@ -354,35 +354,35 @@ class FileTreeList {
|
||||
|
||||
if (hasChildren) {
|
||||
html += `<div class="cpack-tree-children" style="display: ${isRoot ? '' : 'none'}">`;
|
||||
|
||||
|
||||
// 渲染子目录
|
||||
for (const dir of dirs) {
|
||||
html += this.renderNode(dir);
|
||||
}
|
||||
|
||||
|
||||
// 渲染文件
|
||||
for (const file of node.files) {
|
||||
html += `
|
||||
<div class="cpack-tree-item">
|
||||
<label style="padding-left: 17px">
|
||||
<input type="checkbox" name="files" value="${file.path}"
|
||||
<input type="checkbox" name="files" value="${file.path}"
|
||||
${this.state.isSelected(file.path) || file.checked ? 'checked' : ''} />
|
||||
<span>${file.name}</span>
|
||||
${file.badges ? file.badges.map(badge =>
|
||||
`<span style="background: ${badge.color || '#00a67d33'};
|
||||
color: ${badge.textColor || '#00a67d'};
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
${file.badges ? file.badges.map(badge =>
|
||||
`<span style="background: ${badge.color || '#00a67d33'};
|
||||
color: ${badge.textColor || '#00a67d'};
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
cursor: help;
|
||||
white-space: nowrap;"
|
||||
white-space: nowrap;"
|
||||
title="${badge.tooltip || ''}">${badge.text}</span>`
|
||||
).join('') : ''}
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ class FileTreeList {
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
|
||||
|
||||
|
||||
// 目录选择功能
|
||||
this.container.querySelectorAll("[data-action='check-dir']").forEach(checkbox => {
|
||||
@@ -433,7 +433,7 @@ class FileTreeList {
|
||||
const childFiles = Array.from(children.querySelectorAll("input[name='files']"))
|
||||
.map(input => input.value);
|
||||
this.state.toggleMultiple(childFiles, e.target.checked);
|
||||
|
||||
|
||||
// 更新UI
|
||||
children.querySelectorAll("input[type='checkbox']").forEach(child => {
|
||||
child.checked = e.target.checked;
|
||||
@@ -442,7 +442,7 @@ class FileTreeList {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -464,7 +464,7 @@ class FileTreeList {
|
||||
this.container.querySelectorAll("input[name='files']").forEach(checkbox => {
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
this.state.toggle(e.target.value, e.target.checked);
|
||||
|
||||
|
||||
// 更新父文件夹状态
|
||||
const parentFolder = checkbox.closest('.cpack-tree-children')
|
||||
?.previousElementSibling
|
||||
@@ -477,7 +477,7 @@ class FileTreeList {
|
||||
}
|
||||
|
||||
updateCount() {
|
||||
|
||||
|
||||
const countSpan = document.querySelector(`[data-files-count='${this.countId}']`);
|
||||
if (countSpan) {
|
||||
countSpan.textContent = this.state.getSelectedCount();
|
||||
@@ -616,7 +616,7 @@ class ModelList {
|
||||
// 只返回用户选中的模型
|
||||
return Array.from(this.container.querySelectorAll("input[name='models']:checked"))
|
||||
.map(input => input.value);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,7 +679,7 @@ async function createPackModal() {
|
||||
const packageOptionsContainer = form.querySelector("#package-options-container");
|
||||
const packageOptions = new PackageOptions(form, "pack-models-list", "pack-files-list", true);
|
||||
packageOptionsContainer.innerHTML = packageOptions.getHtml();
|
||||
|
||||
|
||||
packageOptions.init().then(() => {
|
||||
confirmButton.disabled = false;
|
||||
});
|
||||
@@ -775,7 +775,7 @@ async function unpackAction() {
|
||||
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;">
|
||||
@@ -804,13 +804,13 @@ async function unpackAction() {
|
||||
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;
|
||||
}
|
||||
@@ -1001,13 +1001,13 @@ class PackageOptions {
|
||||
async init() {
|
||||
const modelsList = this.container.querySelector(`#${this.modelsListId}`);
|
||||
const filesList = this.container.querySelector(`#${this.filesListId}`);
|
||||
|
||||
|
||||
this.modelListComponent = new ModelList(modelsList, this.modelsListId);
|
||||
this.fileListComponent = new FileTreeList(filesList, this.filesListId);
|
||||
|
||||
const addButton = this.container.querySelector("#add-button");
|
||||
const systemPackagesArray = this.container.querySelector("#system-packages-array");
|
||||
|
||||
|
||||
addButton.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const row = document.createElement("div");
|
||||
@@ -1063,7 +1063,7 @@ const buildForm = `
|
||||
<p style="font-size: 0.85em; color: #888; margin-top: 5px;">
|
||||
Get your API Token at <a href="https://cloud.bentoml.com/signup?from=comfy-pack" target="_blank" style="color: #00a67d;">cloud.bentoml.com</a>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1112,7 +1112,7 @@ function createBuildModal() {
|
||||
const packageOptionsContainer = form.querySelector("#package-options-container");
|
||||
const packageOptions = new PackageOptions(form, "build-models-list", "build-files-list", false);
|
||||
packageOptionsContainer.innerHTML = packageOptions.getHtml();
|
||||
|
||||
|
||||
packageOptions.init().then(() => {
|
||||
confirmButton.disabled = false;
|
||||
});
|
||||
@@ -1234,7 +1234,7 @@ async function createServeStatusModal(url) {
|
||||
// Start status checking
|
||||
const checkInterval = setInterval(async () => {
|
||||
try {
|
||||
const resp = await api.fetchApi("/bentoml/serve/heartbeat", { method: "POST" });
|
||||
const resp = await api.fetchApi("/bentoml/serve/heartbeat");
|
||||
const status = await resp.json();
|
||||
if (status.error) {
|
||||
title.textContent = "Server Error";
|
||||
|
||||
Reference in New Issue
Block a user