Compare commits
116 Commits
draft-v4-c
...
tests/api-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da87651e53 | ||
|
|
416122d61d | ||
|
|
d3c625e791 | ||
|
|
ca2c41783c | ||
|
|
e2a6446585 | ||
|
|
839790b5ab | ||
|
|
58b9946936 | ||
|
|
a19ba22eaf | ||
|
|
117715aa22 | ||
|
|
891a5a85ee | ||
|
|
166debfabb | ||
|
|
7258a09fe5 | ||
|
|
058a436187 | ||
|
|
1950802c55 | ||
|
|
eb52a03372 | ||
|
|
f8aa428be3 | ||
|
|
ec0893f136 | ||
|
|
92b99ea963 | ||
|
|
02cd52bb65 | ||
|
|
af1ec2c87b | ||
|
|
41006c3a33 | ||
|
|
116a6d500d | ||
|
|
87d0ac807f | ||
|
|
fc943172eb | ||
|
|
9daa5a2fbd | ||
|
|
b7b2746a61 | ||
|
|
d66a4fbfc8 | ||
|
|
683a172ad8 | ||
|
|
6e12358f5a | ||
|
|
8bcf16dc90 | ||
|
|
65c0a2a1f5 | ||
|
|
115236eb9c | ||
|
|
08de942abe | ||
|
|
e9dff83290 | ||
|
|
3bc6c7584d | ||
|
|
22a2bf1584 | ||
|
|
79ece5f72c | ||
|
|
5da6fe1373 | ||
|
|
48c10d0b95 | ||
|
|
9bb56b1457 | ||
|
|
83420fd828 | ||
|
|
52f4b9506f | ||
|
|
b501e9b20b | ||
|
|
1f7ae5319a | ||
|
|
68c201239d | ||
|
|
6e4e43f612 | ||
|
|
81c3708f39 | ||
|
|
f4d2bbde34 | ||
|
|
d14b42a42c | ||
|
|
0e9c32344c | ||
|
|
30c4ea06af | ||
|
|
8211264993 | ||
|
|
67cf5b49e1 | ||
|
|
8e7ba18e05 | ||
|
|
8359e1063e | ||
|
|
ca078e54b9 | ||
|
|
f7e930c5a2 | ||
|
|
479d95e1c8 | ||
|
|
2b0ff08eef | ||
|
|
67a487db15 | ||
|
|
2488cb3458 | ||
|
|
157e6336fa | ||
|
|
d808a1f406 | ||
|
|
2bb4d8cd63 | ||
|
|
a8164e1631 | ||
|
|
a31d286945 | ||
|
|
12eeef4cf0 | ||
|
|
ce8e6dc36e | ||
|
|
7a32e544a7 | ||
|
|
e16e9d7a0e | ||
|
|
821f908dbc | ||
|
|
e007e6f897 | ||
|
|
94f496fd65 | ||
|
|
d2ce35d2e6 | ||
|
|
2eeebb32dc | ||
|
|
f6d636d82f | ||
|
|
0cd397623e | ||
|
|
5978b6c9ee | ||
|
|
9e132811bc | ||
|
|
3a3b5c1f92 | ||
|
|
26be01ff82 | ||
|
|
8f6dd92374 | ||
|
|
d50b71a887 | ||
|
|
3bc9cbc767 | ||
|
|
b6f6b4fd8a | ||
|
|
a66bada8a3 | ||
|
|
a804f7de19 | ||
|
|
72a61a9966 | ||
|
|
b08bb658ea | ||
|
|
7b28bf608b | ||
|
|
b57747fdf1 | ||
|
|
0735271b10 | ||
|
|
770cd0f9f5 | ||
|
|
32b6266dd9 | ||
|
|
2a8412a2bf | ||
|
|
0c4d289002 | ||
|
|
cee01fec25 | ||
|
|
f00686f3f2 | ||
|
|
bd33f7726e | ||
|
|
22ab526b0c | ||
|
|
af269d198d | ||
|
|
995ef6356e | ||
|
|
aa3bf77c28 | ||
|
|
15667c1259 | ||
|
|
c7b6b565da | ||
|
|
3214ab52c6 | ||
|
|
e3062ff613 | ||
|
|
036b63efe7 | ||
|
|
8d3e1d60d0 | ||
|
|
59876452f4 | ||
|
|
04972ad87f | ||
|
|
c7e69f4e26 | ||
|
|
7a59b6d0d9 | ||
|
|
d227ad97a4 | ||
|
|
b93a474dae | ||
|
|
a5fe075bf3 |
@@ -1 +0,0 @@
|
||||
PYPI_TOKEN=your-pypi-token
|
||||
58
.github/workflows/publish-to-pypi.yml
vendored
58
.github/workflows/publish-to-pypi.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ltdrdata' || github.repository_owner == 'Comfy-Org' }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install build twine
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
CURRENT_VERSION=$(grep -oP 'version = "\K[^"]+' pyproject.toml)
|
||||
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
files: dist/*
|
||||
tag_name: v${{ steps.current_version.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
skip-existing: true
|
||||
verbose: true
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ltdrdata' || github.repository_owner == 'Comfy-Org' }}
|
||||
if: ${{ github.repository_owner == 'ltdrdata' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -17,7 +17,4 @@ github-stats-cache.json
|
||||
pip_overrides.json
|
||||
*.json
|
||||
check2.sh
|
||||
/venv/
|
||||
build
|
||||
*.egg-info
|
||||
.env
|
||||
/venv/
|
||||
@@ -1,47 +0,0 @@
|
||||
## Testing Changes
|
||||
|
||||
1. Activate the ComfyUI environment.
|
||||
|
||||
2. Build package locally after making changes.
|
||||
|
||||
```bash
|
||||
# from inside the ComfyUI-Manager directory, with the ComfyUI environment activated
|
||||
python -m build
|
||||
```
|
||||
|
||||
3. Install the package locally in the ComfyUI environment.
|
||||
|
||||
```bash
|
||||
# Uninstall existing package
|
||||
pip uninstall comfyui-manager
|
||||
|
||||
# Install the locale package
|
||||
pip install dist/comfyui-manager-*.whl
|
||||
```
|
||||
|
||||
4. Start ComfyUI.
|
||||
|
||||
```bash
|
||||
# after navigating to the ComfyUI directory
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Manually Publish Test Version to PyPi
|
||||
|
||||
1. Set the `PYPI_TOKEN` environment variable in env file.
|
||||
|
||||
2. If manually publishing, you likely want to use a release candidate version, so set the version in [pyproject.toml](pyproject.toml) to something like `0.0.1rc1`.
|
||||
|
||||
3. Build the package.
|
||||
|
||||
```bash
|
||||
python -m build
|
||||
```
|
||||
|
||||
4. Upload the package to PyPi.
|
||||
|
||||
```bash
|
||||
python -m twine upload dist/* --username __token__ --password $PYPI_TOKEN
|
||||
```
|
||||
|
||||
5. View at https://pypi.org/project/comfyui-manager/
|
||||
@@ -1,7 +0,0 @@
|
||||
include comfyui_manager/js/*
|
||||
include comfyui_manager/*.json
|
||||
include comfyui_manager/glob/*
|
||||
include LICENSE.txt
|
||||
include README.md
|
||||
include requirements.txt
|
||||
include pyproject.toml
|
||||
81
README.md
81
README.md
@@ -5,7 +5,6 @@
|
||||

|
||||
|
||||
## NOTICE
|
||||
* V4.0: Modify the structure to be installable via pip instead of using git clone.
|
||||
* V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`.
|
||||
* V3.10: `double-click feature` is removed
|
||||
* This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper
|
||||
@@ -14,26 +13,78 @@
|
||||
|
||||
## Installation
|
||||
|
||||
* When installing the latest ComfyUI, it will be automatically installed as a dependency, so manual installation is no longer necessary.
|
||||
### Installation[method1] (General installation method: ComfyUI-Manager only)
|
||||
|
||||
* Manual installation of the nightly version:
|
||||
* Clone to a temporary directory (**Note:** Do **not** clone into `ComfyUI/custom_nodes`.)
|
||||
```
|
||||
git clone https://github.com/Comfy-Org/ComfyUI-Manager
|
||||
```
|
||||
* Install via pip
|
||||
```
|
||||
cd ComfyUI-Manager
|
||||
pip install .
|
||||
```
|
||||
To install ComfyUI-Manager in addition to an existing installation of ComfyUI, you can follow the following steps:
|
||||
|
||||
1. goto `ComfyUI/custom_nodes` dir in terminal(cmd)
|
||||
2. `git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager`
|
||||
3. Restart ComfyUI
|
||||
|
||||
|
||||
### Installation[method2] (Installation for portable ComfyUI version: ComfyUI-Manager only)
|
||||
1. install git
|
||||
- https://git-scm.com/download/win
|
||||
- standalone version
|
||||
- select option: use windows default console window
|
||||
2. Download [scripts/install-manager-for-portable-version.bat](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-manager-for-portable-version.bat) into installed `"ComfyUI_windows_portable"` directory
|
||||
- Don't click. Right click the link and use save as...
|
||||
3. double click `install-manager-for-portable-version.bat` batch file
|
||||
|
||||

|
||||
|
||||
|
||||
### Installation[method3] (Installation through comfy-cli: install ComfyUI and ComfyUI-Manager at once.)
|
||||
> RECOMMENDED: comfy-cli provides various features to manage ComfyUI from the CLI.
|
||||
|
||||
* **prerequisite: python 3, git**
|
||||
|
||||
Windows:
|
||||
```commandline
|
||||
python -m venv venv
|
||||
venv\Scripts\activate
|
||||
pip install comfy-cli
|
||||
comfy install
|
||||
```
|
||||
|
||||
Linux/OSX:
|
||||
```commandline
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install comfy-cli
|
||||
comfy install
|
||||
```
|
||||
* See also: https://github.com/Comfy-Org/comfy-cli
|
||||
|
||||
|
||||
## Front-end
|
||||
### Installation[method4] (Installation for linux+venv: ComfyUI + ComfyUI-Manager)
|
||||
|
||||
* The built-in front-end of ComfyUI-Manager is the legacy front-end. The front-end for ComfyUI-Manager is now provided via [ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend).
|
||||
* To enable the legacy front-end, set the environment variable `ENABLE_LEGACY_COMFYUI_MANAGER_FRONT` to `true` before running.
|
||||
To install ComfyUI with ComfyUI-Manager on Linux using a venv environment, you can follow these steps:
|
||||
* **prerequisite: python-is-python3, python3-venv, git**
|
||||
|
||||
1. Download [scripts/install-comfyui-venv-linux.sh](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-comfyui-venv-linux.sh) into empty install directory
|
||||
- Don't click. Right click the link and use save as...
|
||||
- ComfyUI will be installed in the subdirectory of the specified directory, and the directory will contain the generated executable script.
|
||||
2. `chmod +x install-comfyui-venv-linux.sh`
|
||||
3. `./install-comfyui-venv-linux.sh`
|
||||
|
||||
### Installation Precautions
|
||||
* **DO**: `ComfyUI-Manager` files must be accurately located in the path `ComfyUI/custom_nodes/comfyui-manager`
|
||||
* Installing in a compressed file format is not recommended.
|
||||
* **DON'T**: Decompress directly into the `ComfyUI/custom_nodes` location, resulting in the Manager contents like `__init__.py` being placed directly in that directory.
|
||||
* You have to remove all ComfyUI-Manager files from `ComfyUI/custom_nodes`
|
||||
* **DON'T**: In a form where decompression occurs in a path such as `ComfyUI/custom_nodes/ComfyUI-Manager/ComfyUI-Manager`.
|
||||
* **DON'T**: In a form where decompression occurs in a path such as `ComfyUI/custom_nodes/ComfyUI-Manager-main`.
|
||||
* In such cases, `ComfyUI-Manager` may operate, but it won't be recognized within `ComfyUI-Manager`, and updates cannot be performed. It also poses the risk of duplicate installations. Remove it and install properly via `git clone` method.
|
||||
|
||||
|
||||
You can execute ComfyUI by running either `./run_gpu.sh` or `./run_cpu.sh` depending on your system configuration.
|
||||
|
||||
## Colab Notebook
|
||||
This repository provides Colab notebooks that allow you to install and use ComfyUI, including ComfyUI-Manager. To use ComfyUI, [click on this link](https://colab.research.google.com/github/ltdrdata/ComfyUI-Manager/blob/main/notebooks/comfyui_colab_with_manager.ipynb).
|
||||
* Support for installing ComfyUI
|
||||
* Support for basic installation of ComfyUI-Manager
|
||||
* Support for automatically installing dependencies of custom nodes upon restarting Colab notebooks.
|
||||
|
||||
|
||||
## How To Use
|
||||
|
||||
25
__init__.py
Normal file
25
__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
This file is the entry point for the ComfyUI-Manager package, handling CLI-only mode and initial setup.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode')
|
||||
|
||||
if not os.path.exists(cli_mode_flag):
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
|
||||
import manager_server # noqa: F401
|
||||
import share_3rdparty # noqa: F401
|
||||
import cm_global
|
||||
|
||||
if not cm_global.disable_front and not 'DISABLE_COMFYUI_MANAGER_FRONT' in os.environ:
|
||||
WEB_DIRECTORY = "js"
|
||||
else:
|
||||
print("\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n")
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
__all__ = ['NODE_CLASS_MAPPINGS']
|
||||
|
||||
|
||||
|
||||
@@ -15,36 +15,41 @@ import git
|
||||
import importlib
|
||||
|
||||
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
|
||||
|
||||
import manager_util
|
||||
|
||||
# read env vars
|
||||
# COMFYUI_FOLDERS_BASE_PATH is not required in cm-cli.py
|
||||
# `comfy_path` should be resolved before importing manager_core
|
||||
|
||||
comfy_path = os.environ.get('COMFYUI_PATH')
|
||||
|
||||
if comfy_path is None:
|
||||
print("[bold red]cm-cli: environment variable 'COMFYUI_PATH' is not specified.[/bold red]")
|
||||
exit(-1)
|
||||
try:
|
||||
import folder_paths
|
||||
comfy_path = os.path.join(os.path.dirname(folder_paths.__file__))
|
||||
except:
|
||||
print("\n[bold yellow]WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.[/bold yellow]", file=sys.stderr)
|
||||
comfy_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..', '..'))
|
||||
|
||||
# This should be placed here
|
||||
sys.path.append(comfy_path)
|
||||
|
||||
if not os.path.exists(os.path.join(comfy_path, 'folder_paths.py')):
|
||||
print("[bold red]cm-cli: '{comfy_path}' is not a valid 'COMFYUI_PATH' location.[/bold red]")
|
||||
exit(-1)
|
||||
|
||||
|
||||
import utils.extra_config
|
||||
from .glob import cm_global
|
||||
from .glob import manager_core as core
|
||||
from .glob.manager_core import unified_manager
|
||||
from .glob import cnr_utils
|
||||
import cm_global
|
||||
import manager_core as core
|
||||
from manager_core import unified_manager
|
||||
import cnr_utils
|
||||
|
||||
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
cm_global.pip_blacklist = {'torch', 'torchsde', 'torchvision'}
|
||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
|
||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||
else:
|
||||
cm_global.pip_overrides = {}
|
||||
|
||||
if os.path.exists(os.path.join(manager_util.comfyui_manager_path, "pip_overrides.json")):
|
||||
with open(os.path.join(manager_util.comfyui_manager_path, "pip_overrides.json"), 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
@@ -146,7 +151,9 @@ class Ctx:
|
||||
if os.path.exists(core.manager_pip_overrides_path):
|
||||
with open(core.manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
cm_global.pip_overrides = json.load(json_file)
|
||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||
|
||||
if os.path.exists(core.manager_pip_blacklist_path):
|
||||
with open(core.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
|
||||
@@ -183,13 +190,18 @@ class Ctx:
|
||||
cmd_ctx = Ctx()
|
||||
|
||||
|
||||
def install_node(node_spec_str, is_all=False, cnt_msg=''):
|
||||
def install_node(node_spec_str, is_all=False, cnt_msg='', **kwargs):
|
||||
exit_on_fail = kwargs.get('exit_on_fail', False)
|
||||
print(f"install_node exit on fail:{exit_on_fail}...")
|
||||
|
||||
if core.is_valid_url(node_spec_str):
|
||||
# install via urls
|
||||
res = asyncio.run(core.gitclone_install(node_spec_str, no_deps=cmd_ctx.no_deps))
|
||||
if not res.result:
|
||||
print(res.msg)
|
||||
print(f"[bold red]ERROR: An error occurred while installing '{node_spec_str}'.[/bold red]")
|
||||
if exit_on_fail:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"{cnt_msg} [INSTALLED] {node_spec_str:50}")
|
||||
else:
|
||||
@@ -224,6 +236,8 @@ def install_node(node_spec_str, is_all=False, cnt_msg=''):
|
||||
print("")
|
||||
else:
|
||||
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.\n{res.msg}[/bold red]")
|
||||
if exit_on_fail:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def reinstall_node(node_spec_str, is_all=False, cnt_msg=''):
|
||||
@@ -585,7 +599,7 @@ def get_all_installed_node_specs():
|
||||
return res
|
||||
|
||||
|
||||
def for_each_nodes(nodes, act, allow_all=True):
|
||||
def for_each_nodes(nodes, act, allow_all=True, **kwargs):
|
||||
is_all = False
|
||||
if allow_all and 'all' in nodes:
|
||||
is_all = True
|
||||
@@ -597,7 +611,7 @@ def for_each_nodes(nodes, act, allow_all=True):
|
||||
i = 1
|
||||
for x in nodes:
|
||||
try:
|
||||
act(x, is_all=is_all, cnt_msg=f'{i}/{total}')
|
||||
act(x, is_all=is_all, cnt_msg=f'{i}/{total}', **kwargs)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
traceback.print_exc()
|
||||
@@ -641,13 +655,17 @@ def install(
|
||||
None,
|
||||
help="user directory"
|
||||
),
|
||||
exit_on_fail: bool = typer.Option(
|
||||
False,
|
||||
help="Exit on failure"
|
||||
)
|
||||
):
|
||||
cmd_ctx.set_user_directory(user_directory)
|
||||
cmd_ctx.set_channel_mode(channel, mode)
|
||||
cmd_ctx.set_no_deps(no_deps)
|
||||
|
||||
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
|
||||
for_each_nodes(nodes, act=install_node)
|
||||
for_each_nodes(nodes, act=install_node, exit_on_fail=exit_on_fail)
|
||||
pip_fixer.fix_broken()
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/bash
|
||||
python ./comfyui_manager/cm-cli.py $*
|
||||
python cm-cli.py $*
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import os
|
||||
import logging
|
||||
from comfy.cli_args import args
|
||||
|
||||
ENABLE_LEGACY_COMFYUI_MANAGER_FRONT_DEFAULT = True # Enable legacy ComfyUI Manager frontend while new UI is in beta phase
|
||||
|
||||
def prestartup():
|
||||
from . import prestartup_script # noqa: F401
|
||||
logging.info('[PRE] ComfyUI-Manager')
|
||||
|
||||
|
||||
def start():
|
||||
logging.info('[START] ComfyUI-Manager')
|
||||
from .glob import manager_server # noqa: F401
|
||||
from .glob import share_3rdparty # noqa: F401
|
||||
from .glob import cm_global # noqa: F401
|
||||
|
||||
should_show_legacy_manager_front = os.environ.get('ENABLE_LEGACY_COMFYUI_MANAGER_FRONT', 'false') == 'true' or ENABLE_LEGACY_COMFYUI_MANAGER_FRONT_DEFAULT
|
||||
if not args.disable_manager and should_show_legacy_manager_front:
|
||||
try:
|
||||
import nodes
|
||||
nodes.EXTENSION_WEB_DIRS['comfyui-manager-legacy'] = os.path.join(os.path.dirname(__file__), 'js')
|
||||
except Exception as e:
|
||||
print("Error enabling legacy ComfyUI Manager frontend:", e)
|
||||
|
||||
|
||||
def should_be_disabled(fullpath:str) -> bool:
|
||||
"""
|
||||
1. Disables the legacy ComfyUI-Manager.
|
||||
2. The blocklist can be expanded later based on policies.
|
||||
"""
|
||||
|
||||
if not args.disable_manager:
|
||||
# In cases where installation is done via a zip archive, the directory name may not be comfyui-manager, and it may not contain a git repository.
|
||||
# It is assumed that any installed legacy ComfyUI-Manager will have at least 'comfyui-manager' in its directory name.
|
||||
dir_name = os.path.basename(fullpath).lower()
|
||||
if 'comfyui-manager' in dir_name:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -1,17 +0,0 @@
|
||||
import enum
|
||||
|
||||
class NetworkMode(enum.Enum):
|
||||
PUBLIC = "public"
|
||||
PRIVATE = "private"
|
||||
OFFLINE = "offline"
|
||||
|
||||
class SecurityLevel(enum.Enum):
|
||||
STRONG = "strong"
|
||||
NORMAL = "normal"
|
||||
NORMAL_MINUS = "normal-minus"
|
||||
WEAK = "weak"
|
||||
|
||||
class DBMode(enum.Enum):
|
||||
LOCAL = "local"
|
||||
CACHE = "cache"
|
||||
REMOTE = "remote"
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,9 @@ comfy_path = os.environ.get('COMFYUI_PATH')
|
||||
git_exe_path = os.environ.get('GIT_EXE_PATH')
|
||||
|
||||
if comfy_path is None:
|
||||
print("git_helper: environment variable 'COMFYUI_PATH' is not specified.")
|
||||
exit(-1)
|
||||
print("\nWARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.", file=sys.stderr)
|
||||
comfy_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
if not os.path.exists(os.path.join(comfy_path, 'folder_paths.py')):
|
||||
print("git_helper: '{comfy_path}' is not a valid 'COMFYUI_PATH' location.")
|
||||
exit(-1)
|
||||
|
||||
def download_url(url, dest_folder, filename=None):
|
||||
# Ensure the destination folder exists
|
||||
8938
github-stats.json
8938
github-stats.json
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,8 @@ import time
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from . import manager_core
|
||||
from . import manager_util
|
||||
|
||||
import manager_core
|
||||
import manager_util
|
||||
import requests
|
||||
import toml
|
||||
|
||||
@@ -32,15 +32,18 @@ from packaging import version
|
||||
|
||||
import uuid
|
||||
|
||||
from . import cm_global
|
||||
from . import cnr_utils
|
||||
from . import manager_util
|
||||
from . import git_utils
|
||||
from . import manager_downloader
|
||||
from .node_package import InstalledNodePackage
|
||||
from .enums import NetworkMode, SecurityLevel, DBMode
|
||||
glob_path = os.path.join(os.path.dirname(__file__)) # ComfyUI-Manager/glob
|
||||
sys.path.append(glob_path)
|
||||
|
||||
version_code = [4, 0]
|
||||
import cm_global
|
||||
import cnr_utils
|
||||
import manager_util
|
||||
import git_utils
|
||||
import manager_downloader
|
||||
from node_package import InstalledNodePackage
|
||||
|
||||
|
||||
version_code = [3, 32, 3]
|
||||
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
|
||||
|
||||
|
||||
@@ -55,7 +58,6 @@ class InvalidChannel(Exception):
|
||||
self.channel = channel
|
||||
super().__init__(channel)
|
||||
|
||||
|
||||
def get_default_custom_nodes_path():
|
||||
global default_custom_nodes_path
|
||||
if default_custom_nodes_path is None:
|
||||
@@ -181,11 +183,10 @@ comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
|
||||
|
||||
if comfy_path is None:
|
||||
try:
|
||||
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
|
||||
os.environ['COMFYUI_PATH'] = comfy_path
|
||||
import folder_paths
|
||||
comfy_path = os.path.join(os.path.dirname(folder_paths.__file__))
|
||||
except:
|
||||
logging.error("[ComfyUI-Manager] environment variable 'COMFYUI_PATH' is not specified.")
|
||||
exit(-1)
|
||||
comfy_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..', '..'))
|
||||
|
||||
if comfy_base_path is None:
|
||||
comfy_base_path = comfy_path
|
||||
@@ -202,7 +203,6 @@ manager_snapshot_path = None
|
||||
manager_pip_overrides_path = None
|
||||
manager_pip_blacklist_path = None
|
||||
manager_components_path = None
|
||||
manager_batch_history_path = None
|
||||
|
||||
def update_user_directory(user_dir):
|
||||
global manager_files_path
|
||||
@@ -213,7 +213,6 @@ def update_user_directory(user_dir):
|
||||
global manager_pip_overrides_path
|
||||
global manager_pip_blacklist_path
|
||||
global manager_components_path
|
||||
global manager_batch_history_path
|
||||
|
||||
manager_files_path = os.path.abspath(os.path.join(user_dir, 'default', 'ComfyUI-Manager'))
|
||||
if not os.path.exists(manager_files_path):
|
||||
@@ -233,14 +232,10 @@ def update_user_directory(user_dir):
|
||||
manager_pip_blacklist_path = os.path.join(manager_files_path, "pip_blacklist.list")
|
||||
manager_components_path = os.path.join(manager_files_path, "components")
|
||||
manager_util.cache_dir = os.path.join(manager_files_path, "cache")
|
||||
manager_batch_history_path = os.path.join(manager_files_path, "batch_history")
|
||||
|
||||
if not os.path.exists(manager_util.cache_dir):
|
||||
os.makedirs(manager_util.cache_dir)
|
||||
|
||||
if not os.path.exists(manager_batch_history_path):
|
||||
os.makedirs(manager_batch_history_path)
|
||||
|
||||
try:
|
||||
import folder_paths
|
||||
update_user_directory(folder_paths.get_user_directory())
|
||||
@@ -558,7 +553,7 @@ class UnifiedManager:
|
||||
ver = str(manager_util.StrictVersion(info['version']))
|
||||
return {'id': cnr['id'], 'cnr': cnr, 'ver': ver}
|
||||
else:
|
||||
return {'id': info['id'], 'ver': info['version']}
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -734,7 +729,7 @@ class UnifiedManager:
|
||||
|
||||
return latest
|
||||
|
||||
async def reload(self, cache_mode, dont_wait=True, update_cnr_map=True):
|
||||
async def reload(self, cache_mode, dont_wait=True):
|
||||
self.custom_node_map_cache = {}
|
||||
self.cnr_inactive_nodes = {} # node_id -> node_version -> fullpath
|
||||
self.nightly_inactive_nodes = {} # node_id -> fullpath
|
||||
@@ -742,18 +737,17 @@ class UnifiedManager:
|
||||
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
|
||||
self.active_nodes = {} # node_id -> node_version * fullpath
|
||||
|
||||
if get_config()['network_mode'] != 'public' or manager_util.is_manager_pip_package():
|
||||
if get_config()['network_mode'] != 'public':
|
||||
dont_wait = True
|
||||
|
||||
if update_cnr_map:
|
||||
# reload 'cnr_map' and 'repo_cnr_map'
|
||||
cnrs = await cnr_utils.get_cnr_data(cache_mode=cache_mode=='cache', dont_wait=dont_wait)
|
||||
# reload 'cnr_map' and 'repo_cnr_map'
|
||||
cnrs = await cnr_utils.get_cnr_data(cache_mode=cache_mode=='cache', dont_wait=dont_wait)
|
||||
|
||||
for x in cnrs:
|
||||
self.cnr_map[x['id']] = x
|
||||
if 'repository' in x:
|
||||
normalized_url = git_utils.normalize_url(x['repository'])
|
||||
self.repo_cnr_map[normalized_url] = x
|
||||
for x in cnrs:
|
||||
self.cnr_map[x['id']] = x
|
||||
if 'repository' in x:
|
||||
normalized_url = git_utils.normalize_url(x['repository'])
|
||||
self.repo_cnr_map[normalized_url] = x
|
||||
|
||||
# reload node status info from custom_nodes/*
|
||||
for custom_nodes_path in folder_paths.get_folder_paths('custom_nodes'):
|
||||
@@ -874,8 +868,9 @@ class UnifiedManager:
|
||||
package_name = remap_pip_package(line.strip())
|
||||
if package_name and not package_name.startswith('#') and package_name not in self.processed_install:
|
||||
self.processed_install.add(package_name)
|
||||
install_cmd = manager_util.make_pip_cmd(["install", package_name])
|
||||
if package_name.strip() != "" and not package_name.startswith('#'):
|
||||
clean_package_name = package_name.split('#')[0].strip()
|
||||
install_cmd = manager_util.make_pip_cmd(["install", clean_package_name])
|
||||
if clean_package_name != "" and not clean_package_name.startswith('#'):
|
||||
res = res and try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution)
|
||||
|
||||
pip_fixer.fix_broken()
|
||||
@@ -1447,20 +1442,12 @@ class UnifiedManager:
|
||||
return self.unified_enable(node_id, version_spec)
|
||||
|
||||
elif version_spec == 'unknown' or version_spec == 'nightly':
|
||||
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
|
||||
|
||||
if version_spec == 'nightly':
|
||||
# disable cnr nodes
|
||||
if self.is_enabled(node_id, 'cnr'):
|
||||
self.unified_disable(node_id, False)
|
||||
|
||||
# use `repo name` as a dir name instead of `cnr id` if system added nodepack (i.e. publisher is null)
|
||||
cnr = self.cnr_map.get(node_id)
|
||||
|
||||
if cnr is not None and cnr.get('publisher') is None:
|
||||
repo_name = os.path.basename(git_utils.normalize_url(repo_url))
|
||||
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), repo_name))
|
||||
|
||||
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
|
||||
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
|
||||
if res.result:
|
||||
if version_spec == 'unknown':
|
||||
@@ -1677,9 +1664,9 @@ def read_config():
|
||||
'model_download_by_agent': get_bool('model_download_by_agent', False),
|
||||
'downgrade_blacklist': default_conf.get('downgrade_blacklist', '').lower(),
|
||||
'always_lazy_install': get_bool('always_lazy_install', False),
|
||||
'network_mode': default_conf.get('network_mode', NetworkMode.PUBLIC.value).lower(),
|
||||
'security_level': default_conf.get('security_level', SecurityLevel.NORMAL.value).lower(),
|
||||
'db_mode': default_conf.get('db_mode', DBMode.CACHE.value).lower(),
|
||||
'network_mode': default_conf.get('network_mode', 'public').lower(),
|
||||
'security_level': default_conf.get('security_level', 'normal').lower(),
|
||||
'db_mode': default_conf.get('db_mode', 'cache').lower(),
|
||||
}
|
||||
|
||||
except Exception:
|
||||
@@ -1700,9 +1687,9 @@ def read_config():
|
||||
'model_download_by_agent': False,
|
||||
'downgrade_blacklist': '',
|
||||
'always_lazy_install': False,
|
||||
'network_mode': NetworkMode.OFFLINE.value,
|
||||
'security_level': SecurityLevel.NORMAL.value,
|
||||
'db_mode': DBMode.CACHE.value,
|
||||
'network_mode': 'public', # public | private | offline
|
||||
'security_level': 'normal', # strong | normal | normal- | weak
|
||||
'db_mode': 'cache', # local | cache | remote
|
||||
}
|
||||
|
||||
|
||||
@@ -2086,6 +2073,13 @@ def is_valid_url(url):
|
||||
return False
|
||||
|
||||
|
||||
def extract_url_and_commit_id(s):
|
||||
index = s.rfind('@')
|
||||
if index == -1:
|
||||
return (s, '')
|
||||
else:
|
||||
return (s[:index], s[index+1:])
|
||||
|
||||
async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=False):
|
||||
await unified_manager.reload('cache')
|
||||
await unified_manager.get_custom_nodes('default', 'cache')
|
||||
@@ -2103,8 +2097,11 @@ async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=
|
||||
cnr = unified_manager.get_cnr_by_repo(url)
|
||||
if cnr:
|
||||
cnr_id = cnr['id']
|
||||
return await unified_manager.install_by_id(cnr_id, version_spec='nightly', channel='default', mode='cache')
|
||||
return await unified_manager.install_by_id(cnr_id, version_spec=None, channel='default', mode='cache')
|
||||
else:
|
||||
new_url, commit_id = extract_url_and_commit_id(url)
|
||||
if commit_id != "":
|
||||
url = new_url
|
||||
repo_name = os.path.splitext(os.path.basename(url))[0]
|
||||
|
||||
# NOTE: Keep original name as possible if unknown node
|
||||
@@ -2137,6 +2134,10 @@ async def gitclone_install(url, instant_execution=False, msg_prefix='', no_deps=
|
||||
return result.fail(f"Failed to clone '{clone_url}' into '{repo_path}'")
|
||||
else:
|
||||
repo = git.Repo.clone_from(clone_url, repo_path, recursive=True, progress=GitProgress())
|
||||
if commit_id!= "":
|
||||
repo.git.checkout(commit_id)
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
|
||||
repo.git.clear_cache()
|
||||
repo.close()
|
||||
|
||||
@@ -2199,7 +2200,7 @@ async def get_data_by_mode(mode, filename, channel_url=None):
|
||||
cache_uri = str(manager_util.simple_hash(uri))+'_'+filename
|
||||
cache_uri = os.path.join(manager_util.cache_dir, cache_uri)
|
||||
|
||||
if get_config()['network_mode'] == 'offline' or manager_util.is_manager_pip_package():
|
||||
if get_config()['network_mode'] == 'offline':
|
||||
# offline network mode
|
||||
if os.path.exists(cache_uri):
|
||||
json_obj = await manager_util.get_data(cache_uri)
|
||||
@@ -2219,7 +2220,7 @@ async def get_data_by_mode(mode, filename, channel_url=None):
|
||||
with open(cache_uri, "w", encoding='utf-8') as file:
|
||||
json.dump(json_obj, file, indent=4, sort_keys=True)
|
||||
except Exception as e:
|
||||
print(f"[ComfyUI-Manager] Due to a network error, switching to local mode.\n=> {filename} @ {channel_url}/{mode}\n=> {e}")
|
||||
print(f"[ComfyUI-Manager] Due to a network error, switching to local mode.\n=> {filename}\n=> {e}")
|
||||
uri = os.path.join(manager_util.comfyui_manager_path, filename)
|
||||
json_obj = await manager_util.get_data(uri)
|
||||
|
||||
@@ -14,23 +14,18 @@ import git
|
||||
from datetime import datetime
|
||||
|
||||
from server import PromptServer
|
||||
import manager_core as core
|
||||
import manager_util
|
||||
import cm_global
|
||||
import logging
|
||||
import asyncio
|
||||
from collections import deque
|
||||
import queue
|
||||
|
||||
from . import manager_core as core
|
||||
from . import manager_util
|
||||
from . import cm_global
|
||||
from . import manager_downloader
|
||||
import manager_downloader
|
||||
|
||||
|
||||
logging.info(f"### Loading: ComfyUI-Manager ({core.version_str})")
|
||||
|
||||
if not manager_util.is_manager_pip_package():
|
||||
network_mode_description = "offline"
|
||||
else:
|
||||
network_mode_description = core.get_config()['network_mode']
|
||||
logging.info("[ComfyUI-Manager] network_mode: " + network_mode_description)
|
||||
logging.info("[ComfyUI-Manager] network_mode: " + core.get_config()['network_mode'])
|
||||
|
||||
comfy_ui_hash = "-"
|
||||
comfyui_tag = None
|
||||
@@ -160,7 +155,9 @@ class ManagerFuncsInComfyUI(core.ManagerFuncs):
|
||||
|
||||
core.manager_funcs = ManagerFuncsInComfyUI()
|
||||
|
||||
from .manager_downloader import download_url, download_url_with_agent
|
||||
sys.path.append('../..')
|
||||
|
||||
from manager_downloader import download_url, download_url_with_agent
|
||||
|
||||
core.comfy_path = os.path.dirname(folder_paths.__file__)
|
||||
core.js_path = os.path.join(core.comfy_path, "web", "extensions")
|
||||
@@ -394,73 +391,16 @@ def nickname_filter(json_obj):
|
||||
return json_obj
|
||||
|
||||
|
||||
class TaskBatch:
|
||||
def __init__(self, batch_json, tasks, failed):
|
||||
self.nodepack_result = {}
|
||||
self.model_result = {}
|
||||
self.batch_id = batch_json.get('batch_id') if batch_json is not None else None
|
||||
self.batch_json = batch_json
|
||||
self.tasks = tasks
|
||||
self.current_index = 0
|
||||
self.stats = {}
|
||||
self.failed = failed if failed is not None else set()
|
||||
self.is_aborted = False
|
||||
|
||||
def is_done(self):
|
||||
return len(self.tasks) <= self.current_index
|
||||
|
||||
def get_next(self):
|
||||
if self.is_done():
|
||||
return None
|
||||
|
||||
item = self.tasks[self.current_index]
|
||||
self.current_index += 1
|
||||
return item
|
||||
|
||||
def done_count(self):
|
||||
return len(self.nodepack_result) + len(self.model_result)
|
||||
|
||||
def total_count(self):
|
||||
return len(self.tasks)
|
||||
|
||||
def abort(self):
|
||||
self.is_aborted = True
|
||||
|
||||
def finalize(self):
|
||||
if self.batch_id is not None:
|
||||
batch_path = os.path.join(core.manager_batch_history_path, self.batch_id+".json")
|
||||
json_obj = {
|
||||
"batch": self.batch_json,
|
||||
"nodepack_result": self.nodepack_result,
|
||||
"model_result": self.model_result,
|
||||
"failed": list(self.failed)
|
||||
}
|
||||
with open(batch_path, "w") as json_file:
|
||||
json.dump(json_obj, json_file, indent=4)
|
||||
|
||||
|
||||
temp_queue_batch = []
|
||||
task_batch_queue = deque()
|
||||
task_queue = queue.Queue()
|
||||
nodepack_result = {}
|
||||
model_result = {}
|
||||
tasks_in_progress = set()
|
||||
task_worker_lock = threading.Lock()
|
||||
aborted_batch = None
|
||||
|
||||
|
||||
def finalize_temp_queue_batch(batch_json=None, failed=None):
|
||||
"""
|
||||
make temp_queue_batch as a batch snapshot and add to batch_queue
|
||||
"""
|
||||
|
||||
global temp_queue_batch
|
||||
|
||||
if len(temp_queue_batch):
|
||||
batch = TaskBatch(batch_json, temp_queue_batch, failed)
|
||||
task_batch_queue.append(batch)
|
||||
temp_queue_batch = []
|
||||
|
||||
|
||||
async def task_worker():
|
||||
global task_queue
|
||||
global nodepack_result
|
||||
global model_result
|
||||
global tasks_in_progress
|
||||
|
||||
async def do_install(item) -> str:
|
||||
@@ -473,7 +413,8 @@ async def task_worker():
|
||||
return f"Cannot resolve install target: '{node_spec_str}'"
|
||||
|
||||
node_name, version_spec, is_specified = node_spec
|
||||
res = await core.unified_manager.install_by_id(node_name, version_spec, channel, mode, return_postinstall=skip_post_install) # discard post install if skip_post_install mode
|
||||
res = await core.unified_manager.install_by_id(node_name, version_spec, channel, mode, return_postinstall=skip_post_install)
|
||||
# discard post install if skip_post_install mode
|
||||
|
||||
if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']:
|
||||
logging.error(f"[ComfyUI-Manager] Installation failed:\n{res.msg}")
|
||||
@@ -488,11 +429,6 @@ async def task_worker():
|
||||
traceback.print_exc()
|
||||
return f"Installation failed:\n{node_spec_str}"
|
||||
|
||||
async def do_enable(item) -> str:
|
||||
ui_id, cnr_id = item
|
||||
core.unified_manager.unified_enable(cnr_id)
|
||||
return 'success'
|
||||
|
||||
async def do_update(item):
|
||||
ui_id, node_name, node_ver = item
|
||||
|
||||
@@ -651,45 +587,31 @@ async def task_worker():
|
||||
|
||||
return f"Model installation error: {model_url}"
|
||||
|
||||
stats = {}
|
||||
|
||||
while True:
|
||||
with task_worker_lock:
|
||||
if len(task_batch_queue) > 0:
|
||||
cur_batch = task_batch_queue[0]
|
||||
else:
|
||||
logging.info("\n[ComfyUI-Manager] All tasks are completed.")
|
||||
logging.info("\nAfter restarting ComfyUI, please refresh the browser.")
|
||||
done_count = len(nodepack_result) + len(model_result)
|
||||
total_count = done_count + task_queue.qsize()
|
||||
|
||||
res = {'status': 'all-done'}
|
||||
if task_queue.empty():
|
||||
logging.info(f"\n[ComfyUI-Manager] Queued works are completed.\n{stats}")
|
||||
|
||||
PromptServer.instance.send_sync("cm-queue-status", res)
|
||||
|
||||
return
|
||||
|
||||
if cur_batch.is_done():
|
||||
logging.info(f"\n[ComfyUI-Manager] A tasks batch(batch_id={cur_batch.batch_id}) is completed.\nstat={cur_batch.stats}")
|
||||
|
||||
res = {'status': 'batch-done',
|
||||
'nodepack_result': cur_batch.nodepack_result,
|
||||
'model_result': cur_batch.model_result,
|
||||
'total_count': cur_batch.total_count(),
|
||||
'done_count': cur_batch.done_count(),
|
||||
'batch_id': cur_batch.batch_id,
|
||||
'remaining_batch_count': len(task_batch_queue) }
|
||||
|
||||
PromptServer.instance.send_sync("cm-queue-status", res)
|
||||
cur_batch.finalize()
|
||||
task_batch_queue.popleft()
|
||||
continue
|
||||
logging.info("\nAfter restarting ComfyUI, please refresh the browser.")
|
||||
PromptServer.instance.send_sync("cm-queue-status",
|
||||
{'status': 'done',
|
||||
'nodepack_result': nodepack_result, 'model_result': model_result,
|
||||
'total_count': total_count, 'done_count': done_count})
|
||||
nodepack_result = {}
|
||||
task_queue = queue.Queue()
|
||||
return # terminate worker thread
|
||||
|
||||
with task_worker_lock:
|
||||
kind, item = cur_batch.get_next()
|
||||
kind, item = task_queue.get()
|
||||
tasks_in_progress.add((kind, item[0]))
|
||||
|
||||
try:
|
||||
if kind == 'install':
|
||||
msg = await do_install(item)
|
||||
elif kind == 'enable':
|
||||
msg = await do_enable(item)
|
||||
elif kind == 'install-model':
|
||||
msg = await do_install_model(item)
|
||||
elif kind == 'update':
|
||||
@@ -713,131 +635,31 @@ async def task_worker():
|
||||
with task_worker_lock:
|
||||
tasks_in_progress.remove((kind, item[0]))
|
||||
|
||||
ui_id = item[0]
|
||||
if kind == 'install-model':
|
||||
cur_batch.model_result[ui_id] = msg
|
||||
ui_target = "model_manager"
|
||||
elif kind == 'update-main':
|
||||
cur_batch.nodepack_result[ui_id] = msg
|
||||
ui_target = "main"
|
||||
elif kind == 'update-comfyui':
|
||||
cur_batch.nodepack_result['comfyui'] = msg
|
||||
ui_target = "main"
|
||||
elif kind == 'update':
|
||||
cur_batch.nodepack_result[ui_id] = msg['msg']
|
||||
ui_target = "nodepack_manager"
|
||||
else:
|
||||
cur_batch.nodepack_result[ui_id] = msg
|
||||
ui_target = "nodepack_manager"
|
||||
ui_id = item[0]
|
||||
if kind == 'install-model':
|
||||
model_result[ui_id] = msg
|
||||
ui_target = "model_manager"
|
||||
elif kind == 'update-main':
|
||||
nodepack_result[ui_id] = msg
|
||||
ui_target = "main"
|
||||
elif kind == 'update-comfyui':
|
||||
nodepack_result['comfyui'] = msg
|
||||
ui_target = "main"
|
||||
elif kind == 'update':
|
||||
nodepack_result[ui_id] = msg['msg']
|
||||
ui_target = "nodepack_manager"
|
||||
else:
|
||||
nodepack_result[ui_id] = msg
|
||||
ui_target = "nodepack_manager"
|
||||
|
||||
cur_batch.stats[kind] = cur_batch.stats.get(kind, 0) + 1
|
||||
stats[kind] = stats.get(kind, 0) + 1
|
||||
|
||||
PromptServer.instance.send_sync("cm-queue-status",
|
||||
{'status': 'in_progress',
|
||||
'target': item[0],
|
||||
'batch_id': cur_batch.batch_id,
|
||||
'ui_target': ui_target,
|
||||
'total_count': cur_batch.total_count(),
|
||||
'done_count': cur_batch.done_count()})
|
||||
{'status': 'in_progress', 'target': item[0], 'ui_target': ui_target,
|
||||
'total_count': total_count, 'done_count': done_count})
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/batch")
|
||||
async def queue_batch(request):
|
||||
json_data = await request.json()
|
||||
|
||||
failed = set()
|
||||
|
||||
for k, v in json_data.items():
|
||||
if k == 'update_all':
|
||||
await _update_all({'mode': v})
|
||||
|
||||
elif k == 'reinstall':
|
||||
for x in v:
|
||||
res = await _uninstall_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
else:
|
||||
res = await _install_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
|
||||
elif k == 'install':
|
||||
for x in v:
|
||||
res = await _install_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
|
||||
elif k == 'uninstall':
|
||||
for x in v:
|
||||
res = await _uninstall_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
|
||||
elif k == 'update':
|
||||
for x in v:
|
||||
res = await _update_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
|
||||
elif k == 'update_comfyui':
|
||||
await update_comfyui(None)
|
||||
|
||||
elif k == 'disable':
|
||||
for x in v:
|
||||
await _disable_node(x)
|
||||
|
||||
elif k == 'install_model':
|
||||
for x in v:
|
||||
res = await _install_model(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
|
||||
elif k == 'fix':
|
||||
for x in v:
|
||||
res = await _fix_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
|
||||
with task_worker_lock:
|
||||
finalize_temp_queue_batch(json_data, failed)
|
||||
_queue_start()
|
||||
|
||||
return web.json_response({"failed": list(failed)}, content_type='application/json')
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/history_list")
|
||||
async def get_history_list(request):
|
||||
history_path = core.manager_batch_history_path
|
||||
|
||||
try:
|
||||
files = [os.path.join(history_path, f) for f in os.listdir(history_path) if os.path.isfile(os.path.join(history_path, f))]
|
||||
files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
|
||||
history_ids = [os.path.basename(f)[:-5] for f in files]
|
||||
|
||||
return web.json_response({"ids": list(history_ids)}, content_type='application/json')
|
||||
except Exception as e:
|
||||
logging.error(f"[ComfyUI-Manager] /v2/manager/queue/history_list - {e}")
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/history")
|
||||
async def get_history(request):
|
||||
try:
|
||||
json_name = request.rel_url.query["id"]+'.json'
|
||||
batch_path = os.path.join(core.manager_batch_history_path, json_name)
|
||||
|
||||
with open(batch_path, 'r', encoding='utf-8') as file:
|
||||
json_str = file.read()
|
||||
json_obj = json.loads(json_str)
|
||||
return web.json_response(json_obj, content_type='application/json')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[ComfyUI-Manager] /v2/manager/queue/history - {e}")
|
||||
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/customnode/getmappings")
|
||||
@routes.get("/customnode/getmappings")
|
||||
async def fetch_customnode_mappings(request):
|
||||
"""
|
||||
provide unified (node -> node pack) mapping list
|
||||
@@ -873,7 +695,7 @@ async def fetch_customnode_mappings(request):
|
||||
return web.json_response(json_obj, content_type='application/json')
|
||||
|
||||
|
||||
@routes.get("/v2/customnode/fetch_updates")
|
||||
@routes.get("/customnode/fetch_updates")
|
||||
async def fetch_updates(request):
|
||||
try:
|
||||
if request.rel_url.query["mode"] == "local":
|
||||
@@ -900,13 +722,8 @@ async def fetch_updates(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/update_all")
|
||||
@routes.get("/manager/queue/update_all")
|
||||
async def update_all(request):
|
||||
json_data = dict(request.rel_url.query)
|
||||
return await _update_all(json_data)
|
||||
|
||||
|
||||
async def _update_all(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
return web.Response(status=403)
|
||||
@@ -918,13 +735,13 @@ async def _update_all(json_data):
|
||||
|
||||
await core.save_snapshot_with_postfix('autosave')
|
||||
|
||||
if json_data["mode"] == "local":
|
||||
if request.rel_url.query["mode"] == "local":
|
||||
channel = 'local'
|
||||
else:
|
||||
channel = core.get_config()['channel_url']
|
||||
|
||||
await core.unified_manager.reload(json_data["mode"])
|
||||
await core.unified_manager.get_custom_nodes(channel, json_data["mode"])
|
||||
await core.unified_manager.reload(request.rel_url.query["mode"])
|
||||
await core.unified_manager.get_custom_nodes(channel, request.rel_url.query["mode"])
|
||||
|
||||
for k, v in core.unified_manager.active_nodes.items():
|
||||
if k == 'comfyui-manager':
|
||||
@@ -933,7 +750,7 @@ async def _update_all(json_data):
|
||||
continue
|
||||
|
||||
update_item = k, k, v[0]
|
||||
temp_queue_batch.append(("update-main", update_item))
|
||||
task_queue.put(("update-main", update_item))
|
||||
|
||||
for k, v in core.unified_manager.unknown_active_nodes.items():
|
||||
if k == 'comfyui-manager':
|
||||
@@ -942,7 +759,7 @@ async def _update_all(json_data):
|
||||
continue
|
||||
|
||||
update_item = k, k, 'unknown'
|
||||
temp_queue_batch.append(("update-main", update_item))
|
||||
task_queue.put(("update-main", update_item))
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
@@ -993,7 +810,7 @@ def populate_markdown(x):
|
||||
|
||||
# freeze imported version
|
||||
startup_time_installed_node_packs = core.get_installed_node_packs()
|
||||
@routes.get("/v2/customnode/installed")
|
||||
@routes.get("/customnode/installed")
|
||||
async def installed_list(request):
|
||||
mode = request.query.get('mode', 'default')
|
||||
|
||||
@@ -1005,7 +822,7 @@ async def installed_list(request):
|
||||
return web.json_response(res, content_type='application/json')
|
||||
|
||||
|
||||
@routes.get("/v2/customnode/getlist")
|
||||
@routes.get("/customnode/getlist")
|
||||
async def fetch_customnode_list(request):
|
||||
"""
|
||||
provide unified custom node list
|
||||
@@ -1118,7 +935,7 @@ def check_model_installed(json_obj):
|
||||
executor.submit(process_model_phase, item)
|
||||
|
||||
|
||||
@routes.get("/v2/externalmodel/getlist")
|
||||
@routes.get("/externalmodel/getlist")
|
||||
async def fetch_externalmodel_list(request):
|
||||
# The model list is only allowed in the default channel, yet.
|
||||
json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'model-list.json')
|
||||
@@ -1131,14 +948,14 @@ async def fetch_externalmodel_list(request):
|
||||
return web.json_response(json_obj, content_type='application/json')
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/v2/snapshot/getlist")
|
||||
@PromptServer.instance.routes.get("/snapshot/getlist")
|
||||
async def get_snapshot_list(request):
|
||||
items = [f[:-5] for f in os.listdir(core.manager_snapshot_path) if f.endswith('.json')]
|
||||
items.sort(reverse=True)
|
||||
return web.json_response({'items': items}, content_type='application/json')
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/remove")
|
||||
@routes.get("/snapshot/remove")
|
||||
async def remove_snapshot(request):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
@@ -1156,7 +973,7 @@ async def remove_snapshot(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/restore")
|
||||
@routes.get("/snapshot/restore")
|
||||
async def restore_snapshot(request):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
@@ -1182,7 +999,7 @@ async def restore_snapshot(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/get_current")
|
||||
@routes.get("/snapshot/get_current")
|
||||
async def get_current_snapshot_api(request):
|
||||
try:
|
||||
return web.json_response(await core.get_current_snapshot(), content_type='application/json')
|
||||
@@ -1190,7 +1007,7 @@ async def get_current_snapshot_api(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/snapshot/save")
|
||||
@routes.get("/snapshot/save")
|
||||
async def save_snapshot(request):
|
||||
try:
|
||||
await core.save_snapshot_with_postfix('snapshot')
|
||||
@@ -1227,7 +1044,82 @@ def unzip_install(files):
|
||||
return True
|
||||
|
||||
|
||||
@routes.get("/v2/customnode/versions/{node_name}")
|
||||
def copy_install(files, js_path_name=None):
|
||||
for url in files:
|
||||
if url.endswith("/"):
|
||||
url = url[:-1]
|
||||
try:
|
||||
filename = os.path.basename(url)
|
||||
if url.endswith(".py"):
|
||||
download_url(url, core.get_default_custom_nodes_path(), filename)
|
||||
else:
|
||||
path = os.path.join(core.js_path, js_path_name) if js_path_name is not None else core.js_path
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
download_url(url, path, filename)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Install(copy) error: {url} / {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
logging.info("Installation was successful.")
|
||||
return True
|
||||
|
||||
|
||||
def copy_uninstall(files, js_path_name='.'):
|
||||
for url in files:
|
||||
if url.endswith("/"):
|
||||
url = url[:-1]
|
||||
dir_name = os.path.basename(url)
|
||||
base_path = core.get_default_custom_nodes_path() if url.endswith('.py') else os.path.join(core.js_path, js_path_name)
|
||||
file_path = os.path.join(base_path, dir_name)
|
||||
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
elif os.path.exists(file_path + ".disabled"):
|
||||
os.remove(file_path + ".disabled")
|
||||
except Exception as e:
|
||||
logging.error(f"Uninstall(copy) error: {url} / {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
logging.info("Uninstallation was successful.")
|
||||
return True
|
||||
|
||||
|
||||
def copy_set_active(files, is_disable, js_path_name='.'):
|
||||
if is_disable:
|
||||
action_name = "Disable"
|
||||
else:
|
||||
action_name = "Enable"
|
||||
|
||||
for url in files:
|
||||
if url.endswith("/"):
|
||||
url = url[:-1]
|
||||
dir_name = os.path.basename(url)
|
||||
base_path = core.get_default_custom_nodes_path() if url.endswith('.py') else os.path.join(core.js_path, js_path_name)
|
||||
file_path = os.path.join(base_path, dir_name)
|
||||
|
||||
try:
|
||||
if is_disable:
|
||||
current_name = file_path
|
||||
new_name = file_path + ".disabled"
|
||||
else:
|
||||
current_name = file_path + ".disabled"
|
||||
new_name = file_path
|
||||
|
||||
os.rename(current_name, new_name)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"{action_name}(copy) error: {url} / {e}", file=sys.stderr)
|
||||
|
||||
return False
|
||||
|
||||
logging.info(f"{action_name} was successful.")
|
||||
return True
|
||||
|
||||
|
||||
@routes.get("/customnode/versions/{node_name}")
|
||||
async def get_cnr_versions(request):
|
||||
node_name = request.match_info.get("node_name", None)
|
||||
versions = core.cnr_utils.all_versions_of_node(node_name)
|
||||
@@ -1238,7 +1130,7 @@ async def get_cnr_versions(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/customnode/disabled_versions/{node_name}")
|
||||
@routes.get("/customnode/disabled_versions/{node_name}")
|
||||
async def get_disabled_versions(request):
|
||||
node_name = request.match_info.get("node_name", None)
|
||||
versions = []
|
||||
@@ -1254,7 +1146,7 @@ async def get_disabled_versions(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.post("/v2/customnode/import_fail_info")
|
||||
@routes.post("/customnode/import_fail_info")
|
||||
async def import_fail_info(request):
|
||||
json_data = await request.json()
|
||||
|
||||
@@ -1271,74 +1163,42 @@ async def import_fail_info(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/reinstall")
|
||||
@routes.post("/manager/queue/reinstall")
|
||||
async def reinstall_custom_node(request):
|
||||
await uninstall_custom_node(request)
|
||||
await install_custom_node(request)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/reset")
|
||||
@routes.get("/manager/queue/reset")
|
||||
async def reset_queue(request):
|
||||
global task_batch_queue
|
||||
global temp_queue_batch
|
||||
|
||||
with task_worker_lock:
|
||||
temp_queue_batch = []
|
||||
task_batch_queue = deque()
|
||||
|
||||
global task_queue
|
||||
task_queue = queue.Queue()
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/abort_current")
|
||||
async def abort_queue(request):
|
||||
global task_batch_queue
|
||||
global temp_queue_batch
|
||||
|
||||
with task_worker_lock:
|
||||
temp_queue_batch = []
|
||||
if len(task_batch_queue) > 0:
|
||||
task_batch_queue[0].abort()
|
||||
task_batch_queue.popleft()
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/status")
|
||||
@routes.get("/manager/queue/status")
|
||||
async def queue_count(request):
|
||||
global task_queue
|
||||
|
||||
with task_worker_lock:
|
||||
if len(task_batch_queue) > 0:
|
||||
cur_batch = task_batch_queue[0]
|
||||
done_count = cur_batch.done_count()
|
||||
total_count = cur_batch.total_count()
|
||||
in_progress_count = len(tasks_in_progress)
|
||||
is_processing = task_worker_thread is not None and task_worker_thread.is_alive()
|
||||
else:
|
||||
done_count = 0
|
||||
total_count = 0
|
||||
in_progress_count = 0
|
||||
is_processing = False
|
||||
done_count = len(nodepack_result) + len(model_result)
|
||||
in_progress_count = len(tasks_in_progress)
|
||||
total_count = done_count + in_progress_count + task_queue.qsize()
|
||||
is_processing = task_worker_thread is not None and task_worker_thread.is_alive()
|
||||
|
||||
return web.json_response({
|
||||
'total_count': total_count,
|
||||
'done_count': done_count,
|
||||
'in_progress_count': in_progress_count,
|
||||
'total_count': total_count, 'done_count': done_count, 'in_progress_count': in_progress_count,
|
||||
'is_processing': is_processing})
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/install")
|
||||
@routes.post("/manager/queue/install")
|
||||
async def install_custom_node(request):
|
||||
json_data = await request.json()
|
||||
print(f"install={json_data}")
|
||||
return await _install_custom_node(json_data)
|
||||
|
||||
|
||||
async def _install_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
json_data = await request.json()
|
||||
|
||||
# non-nightly cnr is safe
|
||||
risky_level = None
|
||||
cnr_id = json_data.get('id')
|
||||
@@ -1350,10 +1210,8 @@ async def _install_custom_node(json_data):
|
||||
if json_data['version'] != 'unknown' and selected_version != 'unknown':
|
||||
if skip_post_install:
|
||||
if cnr_id in core.unified_manager.nightly_inactive_nodes or cnr_id in core.unified_manager.cnr_inactive_nodes:
|
||||
enable_item = json_data.get('ui_id'), cnr_id
|
||||
temp_queue_batch.append(("enable", enable_item))
|
||||
core.unified_manager.unified_enable(cnr_id)
|
||||
return web.Response(status=200)
|
||||
|
||||
elif selected_version is None:
|
||||
selected_version = 'latest'
|
||||
|
||||
@@ -1366,11 +1224,9 @@ async def _install_custom_node(json_data):
|
||||
if git_url is None:
|
||||
logging.error(f"[ComfyUI-Manager] Following node pack doesn't provide `nightly` version: ${git_url}")
|
||||
return web.Response(status=404, text=f"Following node pack doesn't provide `nightly` version: ${git_url}")
|
||||
|
||||
elif json_data['version'] != 'unknown' and selected_version == 'unknown':
|
||||
logging.error(f"[ComfyUI-Manager] Invalid installation request: {json_data}")
|
||||
return web.Response(status=400, text="Invalid installation request")
|
||||
|
||||
else:
|
||||
# unknown
|
||||
unknown_name = os.path.basename(json_data['files'][0])
|
||||
@@ -1389,42 +1245,39 @@ async def _install_custom_node(json_data):
|
||||
return web.Response(status=404, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
install_item = json_data.get('ui_id'), node_spec_str, json_data['channel'], json_data['mode'], skip_post_install
|
||||
temp_queue_batch.append(("install", install_item))
|
||||
task_queue.put(("install", install_item))
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
task_worker_thread:threading.Thread = None
|
||||
|
||||
@routes.get("/v2/manager/queue/start")
|
||||
@routes.get("/manager/queue/start")
|
||||
async def queue_start(request):
|
||||
with task_worker_lock:
|
||||
finalize_temp_queue_batch()
|
||||
return _queue_start()
|
||||
|
||||
def _queue_start():
|
||||
global nodepack_result
|
||||
global model_result
|
||||
global task_worker_thread
|
||||
|
||||
if task_worker_thread is not None and task_worker_thread.is_alive():
|
||||
return web.Response(status=201) # already in-progress
|
||||
|
||||
nodepack_result = {}
|
||||
model_result = {}
|
||||
|
||||
task_worker_thread = threading.Thread(target=lambda: asyncio.run(task_worker()))
|
||||
task_worker_thread.start()
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/fix")
|
||||
@routes.post("/manager/queue/fix")
|
||||
async def fix_custom_node(request):
|
||||
json_data = await request.json()
|
||||
return await _fix_custom_node(json_data)
|
||||
|
||||
|
||||
async def _fix_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_GENERAL)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
json_data = await request.json()
|
||||
|
||||
node_id = json_data.get('id')
|
||||
node_ver = json_data['version']
|
||||
if node_ver != 'unknown':
|
||||
@@ -1434,12 +1287,12 @@ async def _fix_custom_node(json_data):
|
||||
node_name = os.path.basename(json_data['files'][0])
|
||||
|
||||
update_item = json_data.get('ui_id'), node_name, json_data['version']
|
||||
temp_queue_batch.append(("fix", update_item))
|
||||
task_queue.put(("fix", update_item))
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.post("/v2/customnode/install/git_url")
|
||||
@routes.post("/customnode/install/git_url")
|
||||
async def install_custom_node_git_url(request):
|
||||
if not is_allowed_security_level('high'):
|
||||
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
|
||||
@@ -1459,7 +1312,7 @@ async def install_custom_node_git_url(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.post("/v2/customnode/install/pip")
|
||||
@routes.post("/customnode/install/pip")
|
||||
async def install_custom_node_pip(request):
|
||||
if not is_allowed_security_level('high'):
|
||||
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
|
||||
@@ -1471,17 +1324,14 @@ async def install_custom_node_pip(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/uninstall")
|
||||
@routes.post("/manager/queue/uninstall")
|
||||
async def uninstall_custom_node(request):
|
||||
json_data = await request.json()
|
||||
return await _uninstall_custom_node(json_data)
|
||||
|
||||
|
||||
async def _uninstall_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
json_data = await request.json()
|
||||
|
||||
node_id = json_data.get('id')
|
||||
if json_data['version'] != 'unknown':
|
||||
is_unknown = False
|
||||
@@ -1492,22 +1342,19 @@ async def _uninstall_custom_node(json_data):
|
||||
node_name = os.path.basename(json_data['files'][0])
|
||||
|
||||
uninstall_item = json_data.get('ui_id'), node_name, is_unknown
|
||||
temp_queue_batch.append(("uninstall", uninstall_item))
|
||||
task_queue.put(("uninstall", uninstall_item))
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/update")
|
||||
@routes.post("/manager/queue/update")
|
||||
async def update_custom_node(request):
|
||||
json_data = await request.json()
|
||||
return await _update_custom_node(json_data)
|
||||
|
||||
|
||||
async def _update_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
json_data = await request.json()
|
||||
|
||||
node_id = json_data.get('id')
|
||||
if json_data['version'] != 'unknown':
|
||||
node_name = node_id
|
||||
@@ -1516,19 +1363,19 @@ async def _update_custom_node(json_data):
|
||||
node_name = os.path.basename(json_data['files'][0])
|
||||
|
||||
update_item = json_data.get('ui_id'), node_name, json_data['version']
|
||||
temp_queue_batch.append(("update", update_item))
|
||||
task_queue.put(("update", update_item))
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/queue/update_comfyui")
|
||||
@routes.get("/manager/queue/update_comfyui")
|
||||
async def update_comfyui(request):
|
||||
is_stable = core.get_config()['update_policy'] != 'nightly-comfyui'
|
||||
temp_queue_batch.append(("update-comfyui", ('comfyui', is_stable)))
|
||||
task_queue.put(("update-comfyui", ('comfyui', is_stable)))
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/comfyui_manager/comfyui_versions")
|
||||
@routes.get("/comfyui_manager/comfyui_versions")
|
||||
async def comfyui_versions(request):
|
||||
try:
|
||||
res, current, latest = core.get_comfyui_versions()
|
||||
@@ -1539,7 +1386,7 @@ async def comfyui_versions(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.get("/v2/comfyui_manager/comfyui_switch_version")
|
||||
@routes.get("/comfyui_manager/comfyui_switch_version")
|
||||
async def comfyui_switch_version(request):
|
||||
try:
|
||||
if "ver" in request.rel_url.query:
|
||||
@@ -1552,14 +1399,10 @@ async def comfyui_switch_version(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/disable")
|
||||
@routes.post("/manager/queue/disable")
|
||||
async def disable_node(request):
|
||||
json_data = await request.json()
|
||||
await _disable_node(json_data)
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
async def _disable_node(json_data):
|
||||
node_id = json_data.get('id')
|
||||
if json_data['version'] != 'unknown':
|
||||
is_unknown = False
|
||||
@@ -1570,7 +1413,9 @@ async def _disable_node(json_data):
|
||||
node_name = os.path.basename(json_data['files'][0])
|
||||
|
||||
update_item = json_data.get('ui_id'), node_name, is_unknown
|
||||
temp_queue_batch.append(("disable", update_item))
|
||||
task_queue.put(("disable", update_item))
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
async def check_whitelist_for_model(item):
|
||||
@@ -1589,13 +1434,10 @@ async def check_whitelist_for_model(item):
|
||||
return False
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/install_model")
|
||||
@routes.post("/manager/queue/install_model")
|
||||
async def install_model(request):
|
||||
json_data = await request.json()
|
||||
return await _install_model(json_data)
|
||||
|
||||
|
||||
async def _install_model(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
@@ -1619,12 +1461,12 @@ async def _install_model(json_data):
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
install_item = json_data.get('ui_id'), json_data
|
||||
temp_queue_batch.append(("install-model", install_item))
|
||||
task_queue.put(("install-model", install_item))
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/preview_method")
|
||||
@routes.get("/manager/preview_method")
|
||||
async def preview_method(request):
|
||||
if "value" in request.rel_url.query:
|
||||
set_preview_method(request.rel_url.query['value'])
|
||||
@@ -1635,7 +1477,7 @@ async def preview_method(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/db_mode")
|
||||
@routes.get("/manager/db_mode")
|
||||
async def db_mode(request):
|
||||
if "value" in request.rel_url.query:
|
||||
set_db_mode(request.rel_url.query['value'])
|
||||
@@ -1647,7 +1489,7 @@ async def db_mode(request):
|
||||
|
||||
|
||||
|
||||
@routes.get("/v2/manager/policy/component")
|
||||
@routes.get("/manager/policy/component")
|
||||
async def component_policy(request):
|
||||
if "value" in request.rel_url.query:
|
||||
set_component_policy(request.rel_url.query['value'])
|
||||
@@ -1658,7 +1500,7 @@ async def component_policy(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/policy/update")
|
||||
@routes.get("/manager/policy/update")
|
||||
async def update_policy(request):
|
||||
if "value" in request.rel_url.query:
|
||||
set_update_policy(request.rel_url.query['value'])
|
||||
@@ -1669,7 +1511,7 @@ async def update_policy(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/channel_url_list")
|
||||
@routes.get("/manager/channel_url_list")
|
||||
async def channel_url_list(request):
|
||||
channels = core.get_channel_dict()
|
||||
if "value" in request.rel_url.query:
|
||||
@@ -1706,7 +1548,7 @@ def add_target_blank(html_text):
|
||||
return modified_html
|
||||
|
||||
|
||||
@routes.get("/v2/manager/notice")
|
||||
@routes.get("/manager/notice")
|
||||
async def get_notice(request):
|
||||
url = "github.com"
|
||||
path = "/ltdrdata/ltdrdata.github.io/wiki/News"
|
||||
@@ -1753,13 +1595,7 @@ async def get_notice(request):
|
||||
return web.Response(text="Unable to retrieve Notice", status=200)
|
||||
|
||||
|
||||
# legacy /manager/notice
|
||||
@routes.get("/manager/notice")
|
||||
async def get_notice_legacy(request):
|
||||
return web.Response(text="""<font color="red">Starting from ComfyUI-Manager V4.0+, it should be installed via pip.<BR><BR>Please remove the ComfyUI-Manager installed in the <font color="white">'custom_nodes'</font> directory.</font>""", status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/reboot")
|
||||
@routes.get("/manager/reboot")
|
||||
def restart(self):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
@@ -1796,7 +1632,7 @@ def restart(self):
|
||||
return os.execv(sys.executable, cmds)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/component/save")
|
||||
@routes.post("/manager/component/save")
|
||||
async def save_component(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
@@ -1826,7 +1662,7 @@ async def save_component(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/component/loads")
|
||||
@routes.post("/manager/component/loads")
|
||||
async def load_components(request):
|
||||
if os.path.exists(core.manager_components_path):
|
||||
try:
|
||||
@@ -1851,7 +1687,7 @@ async def load_components(request):
|
||||
return web.json_response({})
|
||||
|
||||
|
||||
@routes.get("/v2/manager/version")
|
||||
@routes.get("/manager/version")
|
||||
async def get_version(request):
|
||||
return web.Response(text=core.version_str, status=200)
|
||||
|
||||
@@ -1900,7 +1736,7 @@ async def default_cache_update():
|
||||
logging.error(f"[ComfyUI-Manager] Failed to perform initial fetching '{filename}': {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
if core.get_config()['network_mode'] != 'offline' and not manager_util.is_manager_pip_package():
|
||||
if core.get_config()['network_mode'] != 'offline':
|
||||
a = get_cache("custom-node-list.json")
|
||||
b = get_cache("extension-node-map.json")
|
||||
c = get_cache("model-list.json")
|
||||
@@ -1915,8 +1751,6 @@ async def default_cache_update():
|
||||
# load at least once
|
||||
await core.unified_manager.reload('remote', dont_wait=False)
|
||||
await core.unified_manager.get_custom_nodes(channel_url, 'remote')
|
||||
else:
|
||||
await core.unified_manager.reload('remote', dont_wait=False, update_cnr_map=False)
|
||||
|
||||
logging.info("[ComfyUI-Manager] All startup tasks have been completed.")
|
||||
|
||||
@@ -1934,3 +1768,4 @@ cm_global.register_extension('ComfyUI-Manager',
|
||||
'nodes': {},
|
||||
'description': 'This extension provides the ability to manage custom nodes in ComfyUI.', })
|
||||
|
||||
|
||||
@@ -15,18 +15,16 @@ import re
|
||||
import logging
|
||||
import platform
|
||||
import shlex
|
||||
import cm_global
|
||||
|
||||
|
||||
cache_lock = threading.Lock()
|
||||
session_lock = threading.Lock()
|
||||
|
||||
comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also updated together in **manager_core.update_user_directory**.
|
||||
|
||||
use_uv = False
|
||||
|
||||
def is_manager_pip_package():
|
||||
return not os.path.exists(os.path.join(comfyui_manager_path, '..', 'custom_nodes'))
|
||||
|
||||
def add_python_path_to_env():
|
||||
if platform.system() != "Windows":
|
||||
@@ -259,7 +257,7 @@ def get_installed_packages(renew=False):
|
||||
pip_map[normalized_name] = y[1]
|
||||
except subprocess.CalledProcessError:
|
||||
logging.error("[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.")
|
||||
return set()
|
||||
return {}
|
||||
|
||||
return pip_map
|
||||
|
||||
@@ -310,6 +308,7 @@ def parse_requirement_line(line):
|
||||
|
||||
|
||||
torch_torchvision_torchaudio_version_map = {
|
||||
'2.7.0': ('0.22.0', '2.7.0'),
|
||||
'2.6.0': ('0.21.0', '2.6.0'),
|
||||
'2.5.1': ('0.20.0', '2.5.0'),
|
||||
'2.5.0': ('0.20.0', '2.5.0'),
|
||||
@@ -413,8 +412,9 @@ class PIPFixer:
|
||||
|
||||
if len(targets) > 0:
|
||||
for x in targets:
|
||||
cmd = make_pip_cmd(['install', f"{x}=={versions[0].version_string}", "numpy<2"])
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
if sys.version_info < (3, 13):
|
||||
cmd = make_pip_cmd(['install', f"{x}=={versions[0].version_string}", "numpy<2"])
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
|
||||
logging.info(f"[ComfyUI-Manager] 'opencv' dependencies were fixed: {targets}")
|
||||
except Exception as e:
|
||||
@@ -422,17 +422,21 @@ class PIPFixer:
|
||||
logging.error(e)
|
||||
|
||||
# fix numpy
|
||||
try:
|
||||
np = new_pip_versions.get('numpy')
|
||||
if np is not None:
|
||||
if StrictVersion(np) >= StrictVersion('2'):
|
||||
cmd = make_pip_cmd(['install', "numpy<2"])
|
||||
subprocess.check_output(cmd , universal_newlines=True)
|
||||
if sys.version_info >= (3, 13):
|
||||
logging.info("[ComfyUI-Manager] In Python 3.13 and above, PIP Fixer does not downgrade `numpy` below version 2.0. If you need to force a downgrade of `numpy`, please use `pip_auto_fix.list`.")
|
||||
else:
|
||||
try:
|
||||
np = new_pip_versions.get('numpy')
|
||||
if cm_global.pip_overrides.get('numpy') == 'numpy<2':
|
||||
if np is not None:
|
||||
if StrictVersion(np) >= StrictVersion('2'):
|
||||
cmd = make_pip_cmd(['install', "numpy<2"])
|
||||
subprocess.check_output(cmd , universal_newlines=True)
|
||||
|
||||
logging.info("[ComfyUI-Manager] 'numpy' dependency were fixed")
|
||||
except Exception as e:
|
||||
logging.error("[ComfyUI-Manager] Failed to restore numpy")
|
||||
logging.error(e)
|
||||
logging.info("[ComfyUI-Manager] 'numpy' dependency were fixed")
|
||||
except Exception as e:
|
||||
logging.error("[ComfyUI-Manager] Failed to restore numpy")
|
||||
logging.error(e)
|
||||
|
||||
# fix missing frontend
|
||||
try:
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
|
||||
from .git_utils import get_commit_hash
|
||||
from git_utils import get_commit_hash
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -1,6 +1,5 @@
|
||||
import mimetypes
|
||||
from . import manager_core as core
|
||||
|
||||
import manager_core as core
|
||||
import os
|
||||
from aiohttp import web
|
||||
import aiohttp
|
||||
@@ -54,7 +53,7 @@ def compute_sha256_checksum(filepath):
|
||||
return sha256.hexdigest()
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/v2/manager/share_option")
|
||||
@PromptServer.instance.routes.get("/manager/share_option")
|
||||
async def share_option(request):
|
||||
if "value" in request.rel_url.query:
|
||||
core.get_config()['share_option'] = request.rel_url.query['value']
|
||||
@@ -123,7 +122,7 @@ def set_youml_settings(settings):
|
||||
f.write(settings)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/v2/manager/get_openart_auth")
|
||||
@PromptServer.instance.routes.get("/manager/get_openart_auth")
|
||||
async def api_get_openart_auth(request):
|
||||
# print("Getting stored Matrix credentials...")
|
||||
openart_key = get_openart_auth()
|
||||
@@ -132,7 +131,7 @@ async def api_get_openart_auth(request):
|
||||
return web.json_response({"openart_key": openart_key})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/v2/manager/set_openart_auth")
|
||||
@PromptServer.instance.routes.post("/manager/set_openart_auth")
|
||||
async def api_set_openart_auth(request):
|
||||
json_data = await request.json()
|
||||
openart_key = json_data['openart_key']
|
||||
@@ -141,7 +140,7 @@ async def api_set_openart_auth(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/v2/manager/get_matrix_auth")
|
||||
@PromptServer.instance.routes.get("/manager/get_matrix_auth")
|
||||
async def api_get_matrix_auth(request):
|
||||
# print("Getting stored Matrix credentials...")
|
||||
matrix_auth = get_matrix_auth()
|
||||
@@ -150,7 +149,7 @@ async def api_get_matrix_auth(request):
|
||||
return web.json_response(matrix_auth)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/v2/manager/youml/settings")
|
||||
@PromptServer.instance.routes.get("/manager/youml/settings")
|
||||
async def api_get_youml_settings(request):
|
||||
youml_settings = get_youml_settings()
|
||||
if not youml_settings:
|
||||
@@ -158,14 +157,14 @@ async def api_get_youml_settings(request):
|
||||
return web.json_response(json.loads(youml_settings))
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/v2/manager/youml/settings")
|
||||
@PromptServer.instance.routes.post("/manager/youml/settings")
|
||||
async def api_set_youml_settings(request):
|
||||
json_data = await request.json()
|
||||
set_youml_settings(json.dumps(json_data))
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/v2/manager/get_comfyworkflows_auth")
|
||||
@PromptServer.instance.routes.get("/manager/get_comfyworkflows_auth")
|
||||
async def api_get_comfyworkflows_auth(request):
|
||||
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
|
||||
# in the same directory as the ComfyUI base folder
|
||||
@@ -176,7 +175,7 @@ async def api_get_comfyworkflows_auth(request):
|
||||
return web.json_response({"comfyworkflows_sharekey": comfyworkflows_auth})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/v2/manager/set_esheep_workflow_and_images")
|
||||
@PromptServer.instance.routes.post("/manager/set_esheep_workflow_and_images")
|
||||
async def set_esheep_workflow_and_images(request):
|
||||
json_data = await request.json()
|
||||
with open(os.path.join(core.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
|
||||
@@ -184,7 +183,7 @@ async def set_esheep_workflow_and_images(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/v2/manager/get_esheep_workflow_and_images")
|
||||
@PromptServer.instance.routes.get("/manager/get_esheep_workflow_and_images")
|
||||
async def get_esheep_workflow_and_images(request):
|
||||
with open(os.path.join(core.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
|
||||
data = json.load(file)
|
||||
@@ -212,7 +211,7 @@ def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
|
||||
return comfyworkflows_sharekey.strip()
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/v2/manager/share")
|
||||
@PromptServer.instance.routes.post("/manager/share")
|
||||
async def share_art(request):
|
||||
# get json data
|
||||
json_data = await request.json()
|
||||
@@ -25,7 +25,7 @@ async function tryInstallCustomNode(event) {
|
||||
const res = await customConfirm(msg);
|
||||
if(res) {
|
||||
if(event.detail.target.installed == 'Disabled') {
|
||||
const response = await api.fetchApi(`/v2/customnode/toggle_active`, {
|
||||
const response = await api.fetchApi(`/customnode/toggle_active`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(event.detail.target)
|
||||
@@ -35,7 +35,7 @@ async function tryInstallCustomNode(event) {
|
||||
await sleep(300);
|
||||
app.ui.dialog.show(`Installing... '${event.detail.target.title}'`);
|
||||
|
||||
const response = await api.fetchApi(`/v2/customnode/install`, {
|
||||
const response = await api.fetchApi(`/customnode/install`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(event.detail.target)
|
||||
@@ -52,7 +52,7 @@ async function tryInstallCustomNode(event) {
|
||||
}
|
||||
}
|
||||
|
||||
let response = await api.fetchApi("/v2/manager/reboot");
|
||||
let response = await api.fetchApi("/manager/reboot");
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
return false;
|
||||
@@ -14,9 +14,9 @@ import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
||||
import {
|
||||
free_models, install_pip, install_via_git_url, manager_instance,
|
||||
rebootAPI, setManagerInstance, show_message, customAlert, customPrompt,
|
||||
infoToast, showTerminal, setNeedRestart, generateUUID
|
||||
infoToast, showTerminal, setNeedRestart
|
||||
} from "./common.js";
|
||||
import { ComponentBuilderDialog, load_components, set_component_policy } from "./components-manager.js";
|
||||
import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js";
|
||||
import { CustomNodesManager } from "./custom-nodes-manager.js";
|
||||
import { ModelManager } from "./model-manager.js";
|
||||
import { SnapshotManager } from "./snapshot.js";
|
||||
@@ -189,7 +189,8 @@ docStyle.innerHTML = `
|
||||
}
|
||||
`;
|
||||
|
||||
function isBeforeFrontendVersion(compareVersion) {
|
||||
function is_legacy_front() {
|
||||
let compareVersion = '1.2.49';
|
||||
try {
|
||||
const frontendVersion = window['__COMFYUI_FRONTEND_VERSION__'];
|
||||
if (typeof frontendVersion !== 'string') {
|
||||
@@ -222,9 +223,6 @@ function isBeforeFrontendVersion(compareVersion) {
|
||||
}
|
||||
}
|
||||
|
||||
const is_legacy_front = () => isBeforeFrontendVersion('1.2.49');
|
||||
const isNotNewManagerUI = () => isBeforeFrontendVersion('1.16.4');
|
||||
|
||||
document.head.appendChild(docStyle);
|
||||
|
||||
var update_comfyui_button = null;
|
||||
@@ -234,7 +232,7 @@ var restart_stop_button = null;
|
||||
var update_policy_combo = null;
|
||||
|
||||
let share_option = 'all';
|
||||
var batch_id = null;
|
||||
var is_updating = false;
|
||||
|
||||
|
||||
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
|
||||
@@ -417,7 +415,7 @@ const style = `
|
||||
`;
|
||||
|
||||
async function init_share_option() {
|
||||
api.fetchApi('/v2/manager/share_option')
|
||||
api.fetchApi('/manager/share_option')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
share_option = data || 'all';
|
||||
@@ -425,7 +423,7 @@ async function init_share_option() {
|
||||
}
|
||||
|
||||
async function init_notice(notice) {
|
||||
api.fetchApi('/v2/manager/notice')
|
||||
api.fetchApi('/manager/notice')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
notice.innerHTML = data;
|
||||
@@ -476,19 +474,14 @@ async function updateComfyUI() {
|
||||
let prev_text = update_comfyui_button.innerText;
|
||||
update_comfyui_button.innerText = "Updating ComfyUI...";
|
||||
|
||||
// set_inprogress_mode();
|
||||
set_inprogress_mode();
|
||||
|
||||
const response = await api.fetchApi('/manager/queue/update_comfyui');
|
||||
|
||||
showTerminal();
|
||||
|
||||
batch_id = generateUUID();
|
||||
|
||||
let batch = {};
|
||||
batch['batch_id'] = batch_id;
|
||||
batch['update_comfyui'] = true;
|
||||
|
||||
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(batch)
|
||||
});
|
||||
is_updating = true;
|
||||
await api.fetchApi('/manager/queue/start');
|
||||
}
|
||||
|
||||
function showVersionSelectorDialog(versions, current, onSelect) {
|
||||
@@ -619,7 +612,7 @@ async function switchComfyUI() {
|
||||
switch_comfyui_button.disabled = true;
|
||||
switch_comfyui_button.style.backgroundColor = "gray";
|
||||
|
||||
let res = await api.fetchApi(`/v2/comfyui_manager/comfyui_versions`, { cache: "no-store" });
|
||||
let res = await api.fetchApi(`/comfyui_manager/comfyui_versions`, { cache: "no-store" });
|
||||
|
||||
switch_comfyui_button.disabled = false;
|
||||
switch_comfyui_button.style.backgroundColor = "";
|
||||
@@ -638,14 +631,14 @@ async function switchComfyUI() {
|
||||
showVersionSelectorDialog(versions, obj.current, async (selected_version) => {
|
||||
if(selected_version == 'nightly') {
|
||||
update_policy_combo.value = 'nightly-comfyui';
|
||||
api.fetchApi('/v2/manager/policy/update?value=nightly-comfyui');
|
||||
api.fetchApi('/manager/policy/update?value=nightly-comfyui');
|
||||
}
|
||||
else {
|
||||
update_policy_combo.value = 'stable-comfyui';
|
||||
api.fetchApi('/v2/manager/policy/update?value=stable-comfyui');
|
||||
api.fetchApi('/manager/policy/update?value=stable-comfyui');
|
||||
}
|
||||
|
||||
let response = await api.fetchApi(`/v2/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
|
||||
let response = await api.fetchApi(`/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
|
||||
if (response.status == 200) {
|
||||
infoToast(`ComfyUI version is switched to ${selected_version}`);
|
||||
}
|
||||
@@ -663,17 +656,18 @@ async function onQueueStatus(event) {
|
||||
const isElectron = 'electronAPI' in window;
|
||||
|
||||
if(event.detail.status == 'in_progress') {
|
||||
// set_inprogress_mode();
|
||||
set_inprogress_mode();
|
||||
update_all_button.innerText = `in progress.. (${event.detail.done_count}/${event.detail.total_count})`;
|
||||
}
|
||||
else if(event.detail.status == 'all-done') {
|
||||
else if(event.detail.status == 'done') {
|
||||
reset_action_buttons();
|
||||
}
|
||||
else if(event.detail.status == 'batch-done') {
|
||||
if(batch_id != event.detail.batch_id) {
|
||||
|
||||
if(!is_updating) {
|
||||
return;
|
||||
}
|
||||
|
||||
is_updating = false;
|
||||
|
||||
let success_list = [];
|
||||
let failed_list = [];
|
||||
let comfyui_state = null;
|
||||
@@ -773,28 +767,41 @@ api.addEventListener("cm-queue-status", onQueueStatus);
|
||||
async function updateAll(update_comfyui) {
|
||||
update_all_button.innerText = "Updating...";
|
||||
|
||||
// set_inprogress_mode();
|
||||
set_inprogress_mode();
|
||||
|
||||
var mode = manager_instance.datasrc_combo.value;
|
||||
|
||||
showTerminal();
|
||||
|
||||
batch_id = generateUUID();
|
||||
|
||||
let batch = {};
|
||||
if(update_comfyui) {
|
||||
update_all_button.innerText = "Updating ComfyUI...";
|
||||
batch['update_comfyui'] = true;
|
||||
await api.fetchApi('/manager/queue/update_comfyui');
|
||||
}
|
||||
|
||||
batch['update_all'] = mode;
|
||||
const response = await api.fetchApi(`/manager/queue/update_all?mode=${mode}`);
|
||||
|
||||
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(batch)
|
||||
});
|
||||
if (response.status == 401) {
|
||||
customAlert('Another task is already in progress. Please stop the ongoing task first.');
|
||||
}
|
||||
else if(response.status == 200) {
|
||||
is_updating = true;
|
||||
await api.fetchApi('/manager/queue/start');
|
||||
}
|
||||
}
|
||||
|
||||
function newDOMTokenList(initialTokens) {
|
||||
const tmp = document.createElement(`div`);
|
||||
|
||||
const classList = tmp.classList;
|
||||
if (initialTokens) {
|
||||
initialTokens.forEach(token => {
|
||||
classList.add(token);
|
||||
});
|
||||
}
|
||||
|
||||
return classList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the node is a potential output node (img, gif or video output)
|
||||
*/
|
||||
@@ -807,7 +814,7 @@ function restartOrStop() {
|
||||
rebootAPI();
|
||||
}
|
||||
else {
|
||||
api.fetchApi('/v2/manager/queue/reset');
|
||||
api.fetchApi('/manager/queue/reset');
|
||||
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||
}
|
||||
}
|
||||
@@ -955,12 +962,12 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, []));
|
||||
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'DB: Channel (remote)' }, []));
|
||||
|
||||
api.fetchApi('/v2/manager/db_mode')
|
||||
api.fetchApi('/manager/db_mode')
|
||||
.then(response => response.text())
|
||||
.then(data => { this.datasrc_combo.value = data; });
|
||||
|
||||
this.datasrc_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/v2/manager/db_mode?value=${event.target.value}`);
|
||||
api.fetchApi(`/manager/db_mode?value=${event.target.value}`);
|
||||
});
|
||||
|
||||
// preview method
|
||||
@@ -972,19 +979,19 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Preview method: Latent2RGB (fast)' }, []));
|
||||
preview_combo.appendChild($el('option', { value: 'none', text: 'Preview method: None (very fast)' }, []));
|
||||
|
||||
api.fetchApi('/v2/manager/preview_method')
|
||||
api.fetchApi('/manager/preview_method')
|
||||
.then(response => response.text())
|
||||
.then(data => { preview_combo.value = data; });
|
||||
|
||||
preview_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/v2/manager/preview_method?value=${event.target.value}`);
|
||||
api.fetchApi(`/manager/preview_method?value=${event.target.value}`);
|
||||
});
|
||||
|
||||
// channel
|
||||
let channel_combo = document.createElement("select");
|
||||
channel_combo.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list.");
|
||||
channel_combo.className = "cm-menu-combo";
|
||||
api.fetchApi('/v2/manager/channel_url_list')
|
||||
api.fetchApi('/manager/channel_url_list')
|
||||
.then(response => response.json())
|
||||
.then(async data => {
|
||||
try {
|
||||
@@ -997,7 +1004,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
|
||||
channel_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/v2/manager/channel_url_list?value=${event.target.value}`);
|
||||
api.fetchApi(`/manager/channel_url_list?value=${event.target.value}`);
|
||||
});
|
||||
|
||||
channel_combo.value = data.selected;
|
||||
@@ -1025,7 +1032,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
|
||||
}
|
||||
|
||||
api.fetchApi('/v2/manager/share_option')
|
||||
api.fetchApi('/manager/share_option')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
share_combo.value = data || 'all';
|
||||
@@ -1035,7 +1042,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
share_combo.addEventListener('change', function (event) {
|
||||
const value = event.target.value;
|
||||
share_option = value;
|
||||
api.fetchApi(`/v2/manager/share_option?value=${value}`);
|
||||
api.fetchApi(`/manager/share_option?value=${value}`);
|
||||
const shareButton = document.getElementById("shareButton");
|
||||
if (value === 'none') {
|
||||
shareButton.style.display = "none";
|
||||
@@ -1050,7 +1057,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Component: Use workflow version' }, []));
|
||||
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Component: Use higher version' }, []));
|
||||
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Component: Use my version' }, []));
|
||||
api.fetchApi('/v2/manager/policy/component')
|
||||
api.fetchApi('/manager/policy/component')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
component_policy_combo.value = data;
|
||||
@@ -1058,7 +1065,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
});
|
||||
|
||||
component_policy_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/v2/manager/policy/component?value=${event.target.value}`);
|
||||
api.fetchApi(`/manager/policy/component?value=${event.target.value}`);
|
||||
set_component_policy(event.target.value);
|
||||
});
|
||||
|
||||
@@ -1071,14 +1078,14 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
update_policy_combo.className = "cm-menu-combo";
|
||||
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'Update: ComfyUI Stable Version' }, []));
|
||||
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'Update: ComfyUI Nightly Version' }, []));
|
||||
api.fetchApi('/v2/manager/policy/update')
|
||||
api.fetchApi('/manager/policy/update')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
update_policy_combo.value = data;
|
||||
});
|
||||
|
||||
update_policy_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/v2/manager/policy/update?value=${event.target.value}`);
|
||||
api.fetchApi(`/manager/policy/update?value=${event.target.value}`);
|
||||
});
|
||||
|
||||
return [
|
||||
@@ -1381,12 +1388,12 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
|
||||
async function getVersion() {
|
||||
let version = await api.fetchApi(`/v2/manager/version`);
|
||||
let version = await api.fetchApi(`/manager/version`);
|
||||
return await version.text();
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.Legacy.ManagerMenu",
|
||||
name: "Comfy.ManagerMenu",
|
||||
|
||||
aboutPageBadges: [
|
||||
{
|
||||
@@ -1518,10 +1525,7 @@ app.registerExtension({
|
||||
}).element
|
||||
);
|
||||
|
||||
const shouldShowLegacyMenuItems = isNotNewManagerUI();
|
||||
if (shouldShowLegacyMenuItems) {
|
||||
app.menu?.settingsGroup.element.before(cmGroup.element);
|
||||
}
|
||||
app.menu?.settingsGroup.element.before(cmGroup.element);
|
||||
}
|
||||
catch(exception) {
|
||||
console.log('ComfyUI is outdated. New style menu based features are disabled.');
|
||||
@@ -172,7 +172,7 @@ export const shareToEsheep= () => {
|
||||
const nodes = app.graph._nodes
|
||||
const { potential_outputs, potential_output_nodes } = getPotentialOutputsAndOutputNodes(nodes);
|
||||
const workflow = prompt['workflow']
|
||||
api.fetchApi(`/v2/manager/set_esheep_workflow_and_images`, {
|
||||
api.fetchApi(`/manager/set_esheep_workflow_and_images`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -812,7 +812,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
// get the user's existing matrix auth and share key
|
||||
ShareDialog.matrix_auth = { homeserver: "matrix.org", username: "", password: "" };
|
||||
try {
|
||||
api.fetchApi(`/v2/manager/get_matrix_auth`)
|
||||
api.fetchApi(`/manager/get_matrix_auth`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
ShareDialog.matrix_auth = data;
|
||||
@@ -831,7 +831,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
ShareDialog.cw_sharekey = "";
|
||||
try {
|
||||
// console.log("Fetching comfyworkflows share key")
|
||||
api.fetchApi(`/v2/manager/get_comfyworkflows_auth`)
|
||||
api.fetchApi(`/manager/get_comfyworkflows_auth`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
ShareDialog.cw_sharekey = data.comfyworkflows_sharekey;
|
||||
@@ -891,7 +891,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
// Change the text of the share button to "Sharing..." to indicate that the share process has started
|
||||
this.share_button.textContent = "Sharing...";
|
||||
|
||||
const response = await api.fetchApi(`/v2/manager/share`, {
|
||||
const response = await api.fetchApi(`/manager/share`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -67,7 +67,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
async readKey() {
|
||||
let key = ""
|
||||
try {
|
||||
key = await api.fetchApi(`/v2/manager/get_openart_auth`)
|
||||
key = await api.fetchApi(`/manager/get_openart_auth`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
return data.openart_key;
|
||||
@@ -82,7 +82,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
}
|
||||
|
||||
async saveKey(value) {
|
||||
await api.fetchApi(`/v2/manager/set_openart_auth`, {
|
||||
await api.fetchApi(`/manager/set_openart_auth`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
@@ -399,7 +399,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
form.append("file", uploadFile);
|
||||
try {
|
||||
const res = await this.fetchApi(
|
||||
`/v2/workflows/upload_thumbnail`,
|
||||
`/workflows/upload_thumbnail`,
|
||||
{
|
||||
method: "POST",
|
||||
body: form,
|
||||
@@ -459,7 +459,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
throw new Error("Title is required");
|
||||
}
|
||||
|
||||
const current_snapshot = await api.fetchApi(`/v2/snapshot/get_current`)
|
||||
const current_snapshot = await api.fetchApi(`/snapshot/get_current`)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
// console.log(error);
|
||||
@@ -489,7 +489,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
|
||||
try {
|
||||
const response = await this.fetchApi(
|
||||
"/v2/workflows/publish",
|
||||
"/workflows/publish",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
@@ -179,7 +179,7 @@ export class YouMLShareDialog extends ComfyDialog {
|
||||
async loadToken() {
|
||||
let key = ""
|
||||
try {
|
||||
const response = await api.fetchApi(`/v2/manager/youml/settings`)
|
||||
const response = await api.fetchApi(`/manager/youml/settings`)
|
||||
const settings = await response.json()
|
||||
return settings.token
|
||||
} catch (error) {
|
||||
@@ -188,7 +188,7 @@ export class YouMLShareDialog extends ComfyDialog {
|
||||
}
|
||||
|
||||
async saveToken(value) {
|
||||
await api.fetchApi(`/v2/manager/youml/settings`, {
|
||||
await api.fetchApi(`/manager/youml/settings`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
@@ -380,7 +380,7 @@ export class YouMLShareDialog extends ComfyDialog {
|
||||
try {
|
||||
let snapshotData = null;
|
||||
try {
|
||||
const snapshot = await api.fetchApi(`/v2/snapshot/get_current`)
|
||||
const snapshot = await api.fetchApi(`/snapshot/get_current`)
|
||||
snapshotData = await snapshot.json()
|
||||
} catch (e) {
|
||||
console.error("Failed to get snapshot", e)
|
||||
@@ -172,7 +172,7 @@ export function rebootAPI() {
|
||||
customConfirm("Are you sure you'd like to reboot the server?").then((isConfirmed) => {
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
api.fetchApi("/v2/manager/reboot");
|
||||
api.fetchApi("/manager/reboot");
|
||||
}
|
||||
catch(exception) {}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ export async function install_pip(packages) {
|
||||
if(packages.includes('&'))
|
||||
app.ui.dialog.show(`Invalid PIP package enumeration: '${packages}'`);
|
||||
|
||||
const res = await api.fetchApi("/v2/customnode/install/pip", {
|
||||
const res = await api.fetchApi("/customnode/install/pip", {
|
||||
method: "POST",
|
||||
body: packages,
|
||||
});
|
||||
@@ -245,7 +245,7 @@ export async function install_via_git_url(url, manager_dialog) {
|
||||
|
||||
show_message(`Wait...<BR><BR>Installing '${url}'`);
|
||||
|
||||
const res = await api.fetchApi("/v2/customnode/install/git_url", {
|
||||
const res = await api.fetchApi("/customnode/install/git_url", {
|
||||
method: "POST",
|
||||
body: url,
|
||||
});
|
||||
@@ -630,14 +630,6 @@ export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {
|
||||
});
|
||||
}
|
||||
|
||||
export function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
function initTooltip () {
|
||||
const mouseenterHandler = (e) => {
|
||||
const target = e.target;
|
||||
@@ -64,7 +64,7 @@ function storeGroupNode(name, data, register=true) {
|
||||
}
|
||||
|
||||
export async function load_components() {
|
||||
let data = await api.fetchApi('/v2/manager/component/loads', {method: "POST"});
|
||||
let data = await api.fetchApi('/manager/component/loads', {method: "POST"});
|
||||
let components = await data.json();
|
||||
|
||||
let start_time = Date.now();
|
||||
@@ -222,7 +222,7 @@ async function save_as_component(node, version, author, prefix, nodename, packna
|
||||
pack_map[packname] = component_name;
|
||||
rpack_map[component_name] = subgraph;
|
||||
|
||||
const res = await api.fetchApi('/v2/manager/component/save', {
|
||||
const res = await api.fetchApi('/manager/component/save', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -259,7 +259,7 @@ async function import_component(component_name, component, mode) {
|
||||
workflow: component
|
||||
};
|
||||
|
||||
const res = await api.fetchApi('/v2/manager/component/save', {
|
||||
const res = await api.fetchApi('/manager/component/save', {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", },
|
||||
body: JSON.stringify(body)
|
||||
@@ -709,7 +709,7 @@ app.handleFile = handleFile;
|
||||
|
||||
let current_component_policy = 'workflow';
|
||||
try {
|
||||
api.fetchApi('/v2/manager/policy/component')
|
||||
api.fetchApi('/manager/policy/component')
|
||||
.then(response => response.text())
|
||||
.then(data => { current_component_policy = data; });
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt,
|
||||
sanitizeHTML, infoToast, showTerminal, setNeedRestart,
|
||||
storeColumnWidth, restoreColumnWidth, getTimeAgo, copyText, loadCss,
|
||||
showPopover, hidePopover, generateUUID
|
||||
showPopover, hidePopover
|
||||
} from "./common.js";
|
||||
|
||||
// https://cenfun.github.io/turbogrid/api.html
|
||||
@@ -66,7 +66,7 @@ export class CustomNodesManager {
|
||||
this.id = "cn-manager";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.Legacy.CustomNodesManager",
|
||||
name: "Comfy.CustomNodesManager",
|
||||
afterConfigureGraph: (missingNodeTypes) => {
|
||||
const item = this.getFilterItem(ShowMode.MISSING);
|
||||
if (item) {
|
||||
@@ -459,7 +459,7 @@ export class CustomNodesManager {
|
||||
|
||||
".cn-manager-stop": {
|
||||
click: () => {
|
||||
api.fetchApi('/v2/manager/queue/reset');
|
||||
api.fetchApi('/manager/queue/reset');
|
||||
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||
}
|
||||
},
|
||||
@@ -635,7 +635,7 @@ export class CustomNodesManager {
|
||||
};
|
||||
}
|
||||
|
||||
const response = await api.fetchApi(`/v2/customnode/import_fail_info`, {
|
||||
const response = await api.fetchApi(`/customnode/import_fail_info`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(info)
|
||||
@@ -1243,7 +1243,7 @@ export class CustomNodesManager {
|
||||
async loadNodes(node_packs) {
|
||||
const mode = manager_instance.datasrc_combo.value;
|
||||
this.showStatus(`Loading node mappings (${mode}) ...`);
|
||||
const res = await fetchData(`/v2/customnode/getmappings?mode=${mode}`);
|
||||
const res = await fetchData(`/customnode/getmappings?mode=${mode}`);
|
||||
if (res.error) {
|
||||
console.log(res.error);
|
||||
return;
|
||||
@@ -1395,10 +1395,10 @@ export class CustomNodesManager {
|
||||
this.showLoading();
|
||||
let res;
|
||||
if(is_enable) {
|
||||
res = await api.fetchApi(`/v2/customnode/disabled_versions/${node_id}`, { cache: "no-store" });
|
||||
res = await api.fetchApi(`/customnode/disabled_versions/${node_id}`, { cache: "no-store" });
|
||||
}
|
||||
else {
|
||||
res = await api.fetchApi(`/v2/customnode/versions/${node_id}`, { cache: "no-store" });
|
||||
res = await api.fetchApi(`/customnode/versions/${node_id}`, { cache: "no-store" });
|
||||
}
|
||||
this.hideLoading();
|
||||
|
||||
@@ -1410,15 +1410,16 @@ export class CustomNodesManager {
|
||||
let version_cnt = 0;
|
||||
|
||||
if(!is_enable) {
|
||||
|
||||
if(rowItem.cnr_latest != rowItem.originalData.active_version && obj.length > 0) {
|
||||
versions.push('latest');
|
||||
}
|
||||
|
||||
if(rowItem.originalData.active_version != 'nightly') {
|
||||
versions.push('nightly');
|
||||
default_version = 'nightly';
|
||||
version_cnt++;
|
||||
}
|
||||
|
||||
if(rowItem.cnr_latest != rowItem.originalData.active_version && obj.length > 0) {
|
||||
versions.push('latest');
|
||||
}
|
||||
}
|
||||
|
||||
for(let v of obj) {
|
||||
@@ -1439,6 +1440,13 @@ export class CustomNodesManager {
|
||||
}
|
||||
|
||||
async installNodes(list, btn, title, selected_version) {
|
||||
let stats = await api.fetchApi('/manager/queue/status');
|
||||
stats = await stats.json();
|
||||
if(stats.is_processing) {
|
||||
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { target, label, mode} = btn;
|
||||
|
||||
if(mode === "uninstall") {
|
||||
@@ -1465,9 +1473,9 @@ export class CustomNodesManager {
|
||||
let needRestart = false;
|
||||
let errorMsg = "";
|
||||
|
||||
let target_items = [];
|
||||
await api.fetchApi('/manager/queue/reset');
|
||||
|
||||
let batch = {};
|
||||
let target_items = [];
|
||||
|
||||
for (const hash of list) {
|
||||
const item = this.grid.getRowItemBy("hash", hash);
|
||||
@@ -1510,11 +1518,23 @@ export class CustomNodesManager {
|
||||
api_mode = 'reinstall';
|
||||
}
|
||||
|
||||
if(batch[api_mode]) {
|
||||
batch[api_mode].push(data);
|
||||
}
|
||||
else {
|
||||
batch[api_mode] = [data];
|
||||
const res = await api.fetchApi(`/manager/queue/${api_mode}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (res.status != 200) {
|
||||
errorMsg = `'${item.title}': `;
|
||||
|
||||
if(res.status == 403) {
|
||||
errorMsg += `This action is not allowed with this security level configuration.\n`;
|
||||
} else if(res.status == 404) {
|
||||
errorMsg += `With the current security level configuration, only custom nodes from the <B>"default channel"</B> can be installed.\n`;
|
||||
} else {
|
||||
errorMsg += await res.text() + '\n';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1531,24 +1551,7 @@ export class CustomNodesManager {
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.batch_id = generateUUID();
|
||||
batch['batch_id'] = this.batch_id;
|
||||
|
||||
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(batch)
|
||||
});
|
||||
|
||||
let failed = await res.json();
|
||||
|
||||
if(failed.length > 0) {
|
||||
for(let k in failed) {
|
||||
let hash = failed[k];
|
||||
const item = this.grid.getRowItemBy("hash", hash);
|
||||
errorMsg = `[FAIL] ${item.title}`;
|
||||
}
|
||||
}
|
||||
|
||||
await api.fetchApi('/manager/queue/start');
|
||||
this.showStop();
|
||||
showTerminal();
|
||||
}
|
||||
@@ -1556,9 +1559,6 @@ export class CustomNodesManager {
|
||||
|
||||
async onQueueStatus(event) {
|
||||
let self = CustomNodesManager.instance;
|
||||
// If legacy manager front is not open, return early (using new manager front)
|
||||
if (self.element?.style.display === 'none') return
|
||||
|
||||
if(event.detail.status == 'in_progress' && event.detail.ui_target == 'nodepack_manager') {
|
||||
const hash = event.detail.target;
|
||||
|
||||
@@ -1569,7 +1569,7 @@ export class CustomNodesManager {
|
||||
self.grid.updateCell(item, "action");
|
||||
self.grid.setRowSelected(item, false);
|
||||
}
|
||||
else if(event.detail.status == 'batch-done' && event.detail.batch_id == self.batch_id) {
|
||||
else if(event.detail.status == 'done') {
|
||||
self.hideStop();
|
||||
self.onQueueCompleted(event.detail);
|
||||
}
|
||||
@@ -1745,7 +1745,7 @@ export class CustomNodesManager {
|
||||
async getMissingNodesLegacy(hashMap, missing_nodes) {
|
||||
const mode = manager_instance.datasrc_combo.value;
|
||||
this.showStatus(`Loading missing nodes (${mode}) ...`);
|
||||
const res = await fetchData(`/v2/customnode/getmappings?mode=${mode}`);
|
||||
const res = await fetchData(`/customnode/getmappings?mode=${mode}`);
|
||||
if (res.error) {
|
||||
this.showError(`Failed to get custom node mappings: ${res.error}`);
|
||||
return;
|
||||
@@ -1860,7 +1860,7 @@ export class CustomNodesManager {
|
||||
async getAlternatives() {
|
||||
const mode = manager_instance.datasrc_combo.value;
|
||||
this.showStatus(`Loading alternatives (${mode}) ...`);
|
||||
const res = await fetchData(`/v2/customnode/alternatives?mode=${mode}`);
|
||||
const res = await fetchData(`/customnode/alternatives?mode=${mode}`);
|
||||
if (res.error) {
|
||||
this.showError(`Failed to get alternatives: ${res.error}`);
|
||||
return [];
|
||||
@@ -1908,7 +1908,7 @@ export class CustomNodesManager {
|
||||
infoToast('Fetching updated information. This may take some time if many custom nodes are installed.');
|
||||
}
|
||||
|
||||
const res = await fetchData(`/v2/customnode/getlist?mode=${mode}${skip_update}`);
|
||||
const res = await fetchData(`/customnode/getlist?mode=${mode}${skip_update}`);
|
||||
if (res.error) {
|
||||
this.showError("Failed to get custom node list.");
|
||||
this.hideLoading();
|
||||
@@ -3,7 +3,7 @@ import { $el } from "../../scripts/ui.js";
|
||||
import {
|
||||
manager_instance, rebootAPI,
|
||||
fetchData, md5, icons, show_message, customAlert, infoToast, showTerminal,
|
||||
storeColumnWidth, restoreColumnWidth, loadCss, generateUUID
|
||||
storeColumnWidth, restoreColumnWidth, loadCss
|
||||
} from "./common.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
@@ -81,10 +81,13 @@ export class ModelManager {
|
||||
value: ""
|
||||
}, {
|
||||
label: "Installed",
|
||||
value: "True"
|
||||
value: "installed"
|
||||
}, {
|
||||
label: "Not Installed",
|
||||
value: "False"
|
||||
value: "not_installed"
|
||||
}, {
|
||||
label: "In Workflow",
|
||||
value: "in_workflow"
|
||||
}];
|
||||
|
||||
this.typeList = [{
|
||||
@@ -172,7 +175,7 @@ export class ModelManager {
|
||||
|
||||
".cmm-manager-stop": {
|
||||
click: () => {
|
||||
api.fetchApi('/v2/manager/queue/reset');
|
||||
api.fetchApi('/manager/queue/reset');
|
||||
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||
}
|
||||
},
|
||||
@@ -254,12 +257,31 @@ export class ModelManager {
|
||||
rowFilter: (rowItem) => {
|
||||
|
||||
const searchableColumns = ["name", "type", "base", "description", "filename", "save_path"];
|
||||
const models_extensions = ['.ckpt', '.pt', '.pt2', '.bin', '.pth', '.safetensors', '.pkl', '.sft'];
|
||||
|
||||
let shouldShown = grid.highlightKeywordsFilter(rowItem, searchableColumns, this.keywords);
|
||||
|
||||
if (shouldShown) {
|
||||
if(this.filter && rowItem.installed !== this.filter) {
|
||||
return false;
|
||||
if(this.filter) {
|
||||
if (this.filter == "in_workflow") {
|
||||
rowItem.in_workflow = null;
|
||||
if (Array.isArray(app.graph._nodes)) {
|
||||
app.graph._nodes.forEach((item, i) => {
|
||||
if (Array.isArray(item.widgets_values)) {
|
||||
item.widgets_values.forEach((_item, i) => {
|
||||
if (rowItem.in_workflow === null && _item !== null && models_extensions.includes("." + _item.toString().split('.').pop())) {
|
||||
let filename = _item.match(/([^\/]+)(?=\.\w+$)/)[0];
|
||||
if (grid.highlightKeywordsFilter(rowItem, searchableColumns, filename)) {
|
||||
rowItem.in_workflow = "True";
|
||||
grid.highlightKeywordsFilter(rowItem, searchableColumns, "");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return ((this.filter == "installed" && rowItem.installed == "True") || (this.filter == "not_installed" && rowItem.installed == "False") || (this.filter == "in_workflow" && rowItem.in_workflow == "True"));
|
||||
}
|
||||
|
||||
if(this.type && rowItem.type !== this.type) {
|
||||
@@ -413,15 +435,23 @@ export class ModelManager {
|
||||
}
|
||||
|
||||
async installModels(list, btn) {
|
||||
let stats = await api.fetchApi('/manager/queue/status');
|
||||
|
||||
stats = await stats.json();
|
||||
if(stats.is_processing) {
|
||||
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
|
||||
return;
|
||||
}
|
||||
|
||||
btn.classList.add("cmm-btn-loading");
|
||||
this.showError("");
|
||||
|
||||
let needRefresh = false;
|
||||
let errorMsg = "";
|
||||
|
||||
let target_items = [];
|
||||
await api.fetchApi('/manager/queue/reset');
|
||||
|
||||
let batch = {};
|
||||
let target_items = [];
|
||||
|
||||
for (const item of list) {
|
||||
this.grid.scrollRowIntoView(item);
|
||||
@@ -438,12 +468,21 @@ export class ModelManager {
|
||||
const data = item.originalData;
|
||||
data.ui_id = item.hash;
|
||||
|
||||
const res = await api.fetchApi(`/manager/queue/install_model`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if(batch['install_model']) {
|
||||
batch['install_model'].push(data);
|
||||
}
|
||||
else {
|
||||
batch['install_model'] = [data];
|
||||
if (res.status != 200) {
|
||||
errorMsg = `'${item.name}': `;
|
||||
|
||||
if(res.status == 403) {
|
||||
errorMsg += `This action is not allowed with this security level configuration.\n`;
|
||||
} else {
|
||||
errorMsg += await res.text() + '\n';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,24 +499,7 @@ export class ModelManager {
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.batch_id = generateUUID();
|
||||
batch['batch_id'] = this.batch_id;
|
||||
|
||||
const res = await api.fetchApi(`/v2/manager/queue/batch`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(batch)
|
||||
});
|
||||
|
||||
let failed = await res.json();
|
||||
|
||||
if(failed.length > 0) {
|
||||
for(let k in failed) {
|
||||
let hash = failed[k];
|
||||
const item = self.grid.getRowItemBy("hash", hash);
|
||||
errorMsg = `[FAIL] ${item.title}`;
|
||||
}
|
||||
}
|
||||
|
||||
await api.fetchApi('/manager/queue/start');
|
||||
this.showStop();
|
||||
showTerminal();
|
||||
}
|
||||
@@ -497,7 +519,7 @@ export class ModelManager {
|
||||
// self.grid.updateCell(item, "tg-column-select");
|
||||
self.grid.updateRow(item);
|
||||
}
|
||||
else if(event.detail.status == 'batch-done') {
|
||||
else if(event.detail.status == 'done') {
|
||||
self.hideStop();
|
||||
self.onQueueCompleted(event.detail);
|
||||
}
|
||||
@@ -623,7 +645,7 @@ export class ModelManager {
|
||||
|
||||
const mode = manager_instance.datasrc_combo.value;
|
||||
|
||||
const res = await fetchData(`/v2/externalmodel/getlist?mode=${mode}`);
|
||||
const res = await fetchData(`/externalmodel/getlist?mode=${mode}`);
|
||||
if (res.error) {
|
||||
this.showError("Failed to get external model list.");
|
||||
this.hideLoading();
|
||||
@@ -795,4 +817,4 @@ export class ModelManager {
|
||||
close() {
|
||||
this.element.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ function node_info_copy(src, dest, connect_both, copy_shape) {
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.Legacy.Manager.NodeFixer",
|
||||
name: "Comfy.Manager.NodeFixer",
|
||||
beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
addMenuHandler(nodeType, function (_, options) {
|
||||
options.push({
|
||||
@@ -7,7 +7,7 @@ import { manager_instance, rebootAPI, show_message } from "./common.js";
|
||||
async function restore_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
try {
|
||||
const response = await api.fetchApi(`/v2/snapshot/restore?target=${target}`, { cache: "no-store" });
|
||||
const response = await api.fetchApi(`/snapshot/restore?target=${target}`, { cache: "no-store" });
|
||||
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
@@ -35,7 +35,7 @@ async function restore_snapshot(target) {
|
||||
async function remove_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
try {
|
||||
const response = await api.fetchApi(`/v2/snapshot/remove?target=${target}`, { cache: "no-store" });
|
||||
const response = await api.fetchApi(`/snapshot/remove?target=${target}`, { cache: "no-store" });
|
||||
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
@@ -61,7 +61,7 @@ async function remove_snapshot(target) {
|
||||
|
||||
async function save_current_snapshot() {
|
||||
try {
|
||||
const response = await api.fetchApi('/v2/snapshot/save', { cache: "no-store" });
|
||||
const response = await api.fetchApi('/snapshot/save', { cache: "no-store" });
|
||||
app.ui.dialog.close();
|
||||
return true;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ async function save_current_snapshot() {
|
||||
}
|
||||
|
||||
async function getSnapshotList() {
|
||||
const response = await api.fetchApi(`/v2/snapshot/getlist`);
|
||||
const response = await api.fetchApi(`/snapshot/getlist`);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class WorkflowMetadataExtension {
|
||||
* enabled is true if the node is enabled, false if it is disabled
|
||||
*/
|
||||
async getInstalledNodes() {
|
||||
const res = await api.fetchApi("/v2/customnode/installed");
|
||||
const res = await api.fetchApi("/customnode/installed");
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ class WorkflowMetadataExtension {
|
||||
if (cnr_id === "comfy-core") return; // don't allow hijacking comfy-core name
|
||||
if (cnr_id) nodeProperties.cnr_id = cnr_id;
|
||||
else nodeProperties.aux_id = aux_id;
|
||||
if (ver) nodeProperties.ver = ver;
|
||||
} else if (["nodes", "comfy_extras"].includes(moduleType)) {
|
||||
if (ver) nodeProperties.ver = ver.trim();
|
||||
} else if (["nodes", "comfy_extras", "comfy_api_nodes"].includes(moduleType)) {
|
||||
nodeProperties.cnr_id = "comfy-core";
|
||||
nodeProperties.ver = this.comfyCoreVersion;
|
||||
}
|
||||
105
model-list.json
105
model-list.json
@@ -749,8 +749,8 @@
|
||||
"save_path": "loras/HyperSD/SDXL",
|
||||
"description": "Hyper-SD LoRA (4steps) - SDXL",
|
||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
||||
"filename": "Hyper-SD15-4steps-lora.safetensors",
|
||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-4steps-lora.safetensors",
|
||||
"filename": "Hyper-SDXL-4steps-lora.safetensors",
|
||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SDXL-4steps-lora.safetensors",
|
||||
"size": "787MB"
|
||||
},
|
||||
{
|
||||
@@ -4953,6 +4953,107 @@
|
||||
"filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"size": "6.74GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "lllyasviel/FramePackI2V_HY",
|
||||
"type": "FramePackI2V",
|
||||
"base": "FramePackI2V",
|
||||
"save_path": "diffusers/lllyasviel",
|
||||
"description": "[SNAPSHOT] This is the f1k1_x_g9_f1k1f2k2f16k4_td FramePack for HY. [w/You cannot download this item on ComfyUI-Manager versions below V3.18]",
|
||||
"reference": "https://huggingface.co/lllyasviel/FramePackI2V_HY",
|
||||
"filename": "<huggingface>",
|
||||
"url": "lllyasviel/FramePackI2V_HY",
|
||||
"size": "25.75GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "LTX-Video Spatial Upscaler v0.9.7",
|
||||
"type": "upscale",
|
||||
"base": "upscale",
|
||||
"save_path": "default",
|
||||
"description": "Spatial upscaler model for LTX-Video. This model enhances the spatial resolution of generated videos.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-spatial-upscaler-0.9.7.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-spatial-upscaler-0.9.7.safetensors",
|
||||
"size": "505MB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video Temporal Upscaler v0.9.7",
|
||||
"type": "upscale",
|
||||
"base": "upscale",
|
||||
"save_path": "default",
|
||||
"description": "Temporal upscaler model for LTX-Video. This model enhances the temporal resolution and smoothness of generated videos.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-temporal-upscaler-0.9.7.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-temporal-upscaler-0.9.7.safetensors",
|
||||
"size": "524MB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "High-resolution quality LTX-Video 13B model.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-dev.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-dev.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B FP8 v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Quantized version of the LTX-Video 13B model, optimized for lower VRAM usage while maintaining high quality.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-dev-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-dev-fp8.safetensors",
|
||||
"size": "15.7GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B Distilled v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Distilled version of the LTX-Video 13B model, providing improved efficiency while maintaining high-resolution quality.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-distilled.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B Distilled FP8 v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Quantized distilled version of the LTX-Video 13B model, optimized for even lower VRAM usage while maintaining quality.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-distilled-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-fp8.safetensors",
|
||||
"size": "15.7GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B Distilled LoRA v0.9.7",
|
||||
"type": "lora",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "loras",
|
||||
"description": "A LoRA adapter that transforms the standard LTX-Video 13B model into a distilled version when loaded.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-distilled-lora128.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-lora128.safetensors",
|
||||
"size": "1.33GB"
|
||||
},
|
||||
{
|
||||
"name": "Latent Bridge Matching for Image Relighting",
|
||||
"type": "diffusion_model",
|
||||
"base": "LBM",
|
||||
"save_path": "diffusion_models/LBM",
|
||||
"description": "Latent Bridge Matching (LBM) Relighting model",
|
||||
"reference": "https://huggingface.co/jasperai/LBM_relighting",
|
||||
"filename": "LBM_relighting.safetensors",
|
||||
"url": "https://huggingface.co/jasperai/LBM_relighting/resolve/main/model.safetensors",
|
||||
"size": "5.02GB"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,341 @@
|
||||
},
|
||||
|
||||
|
||||
|
||||
{
|
||||
"author": "syaofox",
|
||||
"title": "ComfyUI_fnodes [REMOVED]",
|
||||
"reference": "https://github.com/syaofox/ComfyUI_fnodes",
|
||||
"files": [
|
||||
"https://github.com/syaofox/ComfyUI_fnodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI_fnodes is a collection of custom nodes designed for ComfyUI. These nodes provide additional functionality that can enhance your ComfyUI workflows.\nFile manipulation tools, Image resizing tools, IPAdapter tools, Image processing tools, Mask tools, Face analysis tools, Sampler tools, Miscellaneous tools"
|
||||
},
|
||||
{
|
||||
"author": "Hangover3832",
|
||||
"title": "ComfyUI-Hangover-Moondream [DEPRECATED]",
|
||||
"reference": "https://github.com/Hangover3832/ComfyUI-Hangover-Moondream",
|
||||
"files": [
|
||||
"https://github.com/Hangover3832/ComfyUI-Hangover-Moondream"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Moondream is a lightweight multimodal large language model.\n[w/WARN:Additional python code will be downloaded from huggingface and executed. You have to trust this creator if you want to use this node!]"
|
||||
},
|
||||
{
|
||||
"author": "Hangover3832",
|
||||
"title": "Recognize Anything Model (RAM) for ComfyUI [DEPRECATED]",
|
||||
"reference": "https://github.com/Hangover3832/ComfyUI-Hangover-Recognize_Anything",
|
||||
"files": [
|
||||
"https://github.com/Hangover3832/ComfyUI-Hangover-Recognize_Anything"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This is an image recognition node for ComfyUI based on the RAM++ model from [a/xinyu1205](https://huggingface.co/xinyu1205).\nThis node outputs a string of tags with all the recognized objects and elements in the image in English or Chinese language.\nFor image tagging and captioning."
|
||||
},
|
||||
{
|
||||
"author": "Hangover3832",
|
||||
"title": "ComfyUI-Hangover-Nodes [DEPRECATED]",
|
||||
"reference": "https://github.com/Hangover3832/ComfyUI-Hangover-Nodes",
|
||||
"files": [
|
||||
"https://github.com/Hangover3832/ComfyUI-Hangover-Nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes: MS kosmos-2 Interrogator, Save Image w/o Metadata, Image Scale Bounding Box. An implementation of Microsoft [a/kosmos-2](https://huggingface.co/microsoft/kosmos-2-patch14-224) image to text transformer."
|
||||
},
|
||||
{
|
||||
"author": "SirLatore",
|
||||
"title": "ComfyUI-IPAdapterWAN [REMOVED]",
|
||||
"reference": "https://github.com/SirLatore/ComfyUI-IPAdapterWAN",
|
||||
"files": [
|
||||
"https://github.com/SirLatore/ComfyUI-IPAdapterWAN"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This extension adapts the [a/InstantX IP-Adapter for SD3.5-Large](https://huggingface.co/InstantX/SD3.5-Large-IP-Adapter) to work with Wan 2.1 and other UNet-based video/image models in ComfyUI.\nUnlike the original SD3 version (which depends on joint_blocks from MMDiT), this version performs sampling-time identity conditioning by dynamically injecting into attention layers — making it compatible with models like Wan 2.1, AnimateDiff, and other non-SD3 pipelines."
|
||||
},
|
||||
{
|
||||
"author": "Jpzz",
|
||||
"title": "ComfyUI-VirtualInteraction [UNSAFE/REMOVED]",
|
||||
"reference": "https://github.com/Jpzz/ComfyUI-VirtualInteraction",
|
||||
"files": [
|
||||
"https://github.com/Jpzz/ComfyUI-VirtualInteraction"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: virtual interaction custom node when using generative movie\n[w/This nodepack contains a node which is reading arbitrary excel file.]"
|
||||
},
|
||||
{
|
||||
"author": "satche",
|
||||
"title": "Prompt Factory [REMOVED]",
|
||||
"reference": "https://github.com/satche/comfyui-prompt-factory",
|
||||
"files": [
|
||||
"https://github.com/satche/comfyui-prompt-factory"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A modular system that adds randomness to prompt generation"
|
||||
},
|
||||
{
|
||||
"author": "MITCAP",
|
||||
"title": "ComfyUI OpenAI DALL-E 3 Node [REMOVED]",
|
||||
"reference": "https://github.com/MITCAP/OpenAI-ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/MITCAP/OpenAI-ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This project provides custom nodes for ComfyUI that integrate with OpenAI's DALL-E 3 and GPT-4o models. The nodes allow users to generate images and describe images using OpenAI's API.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "raspie10032",
|
||||
"title": "ComfyUI NAI Prompt Converter [REMOVED]",
|
||||
"reference": "https://github.com/raspie10032/ComfyUI_RS_NAI_Local_Prompt_converter",
|
||||
"files": [
|
||||
"https://github.com/raspie10032/ComfyUI_RS_NAI_Local_Prompt_converter"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A custom node extension for ComfyUI that enables conversion between different prompt formats: NovelAI V4, ComfyUI, and old NovelAI."
|
||||
},
|
||||
{
|
||||
"author": "holchan",
|
||||
"title": "ComfyUI-ModelDownloader [REMOVED]",
|
||||
"reference": "https://github.com/holchan/ComfyUI-ModelDownloader",
|
||||
"files": [
|
||||
"https://github.com/holchan/ComfyUI-ModelDownloader"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A ComfyUI node to download models(Checkpoints and LoRA) from external links and act as an output standalone node."
|
||||
},
|
||||
{
|
||||
"author": "Kur0butiMegane",
|
||||
"title": "Comfyui-StringUtils [DEPRECATED]",
|
||||
"reference": "https://github.com/Kur0butiMegane/Comfyui-StringUtils",
|
||||
"files": [
|
||||
"https://github.com/Kur0butiMegane/Comfyui-StringUtils"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Prompt Normalizer, String Splitter, String Line Selector, Extract Markup Value"
|
||||
},
|
||||
{
|
||||
"author": "Apache0ne",
|
||||
"title": "ComfyUI-LantentCompose [REMOVED]",
|
||||
"reference": "https://github.com/Apache0ne/ComfyUI-LantentCompose",
|
||||
"files": [
|
||||
"https://github.com/Apache0ne/ComfyUI-LantentCompose"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Interpolate sdxl latents using slerp with and without a mask. use with unsample nodes for best effect.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "jax-explorer",
|
||||
"title": "ComfyUI-H-flow [REMOVED]",
|
||||
"reference": "https://github.com/jax-explorer/ComfyUI-H-flow",
|
||||
"files": [
|
||||
"https://github.com/jax-explorer/ComfyUI-H-flow"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Wan2-1 Image To Video, LLM Task, Save Image, Save Video, Show Text, FluxPro Ultra, IdeogramV2 Turbo, Runway Image To Video, Kling Image To Video, Replace Text, Join Text, Test Image, Test Text"
|
||||
},
|
||||
{
|
||||
"author": "Apache0ne",
|
||||
"title": "SambaNova [REMOVED]",
|
||||
"id": "SambaNovaAPI",
|
||||
"reference": "https://github.com/Apache0ne/SambaNova",
|
||||
"files": [
|
||||
"https://github.com/Apache0ne/SambaNova"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Super Fast LLM's llama3.1-405B,70B,8B and more"
|
||||
},
|
||||
{
|
||||
"author": "Apache0ne",
|
||||
"title": "ComfyUI-EasyUrlLoader [REMOVED]",
|
||||
"id": "easy-url-loader",
|
||||
"reference": "https://github.com/Apache0ne/ComfyUI-EasyUrlLoader",
|
||||
"files": [
|
||||
"https://github.com/Apache0ne/ComfyUI-EasyUrlLoader"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A simple YT downloader node for ComfyUI using video Urls. Can be used with VHS nodes etc."
|
||||
},
|
||||
{
|
||||
"author": "nxt5656",
|
||||
"title": "ComfyUI-Image2OSS [REMOVED]",
|
||||
"reference": "https://github.com/nxt5656/ComfyUI-Image2OSS",
|
||||
"files": [
|
||||
"https://github.com/nxt5656/ComfyUI-Image2OSS"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Upload the image to Alibaba Cloud OSS."
|
||||
},
|
||||
{
|
||||
"author": "ainewsto",
|
||||
"title": "Comfyui_Comfly",
|
||||
"reference": "https://github.com/ainewsto/Comfyui_Comfly",
|
||||
"files": [
|
||||
"https://github.com/ainewsto/Comfyui_Comfly"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Comfly_Mj, Comfly_mjstyle, Comfly_upload, Comfly_Mju, Comfly_Mjv, Comfly_kling_videoPreview\nNOTE: Comfyui_Comfly_v2 is introduced."
|
||||
},
|
||||
{
|
||||
"author": "shinich39",
|
||||
"title": "comfyui-to-inpaint",
|
||||
"reference": "https://github.com/shinich39/comfyui-to-inpaint",
|
||||
"files": [
|
||||
"https://github.com/shinich39/comfyui-to-inpaint"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Send preview image to inpaint workflow."
|
||||
},
|
||||
{
|
||||
"author": "magic-quill",
|
||||
"title": "ComfyUI_MagicQuill [NOT MAINTAINED]",
|
||||
"id": "MagicQuill",
|
||||
"reference": "https://github.com/magic-quill/ComfyUI_MagicQuill",
|
||||
"files": [
|
||||
"https://github.com/magic-quill/ComfyUI_MagicQuill"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Towards GPT-4 like large language and visual assistant.\nNOTE: The current version has not been maintained for a long time and does not work. Please use https://github.com/brantje/ComfyUI_MagicQuill instead."
|
||||
},
|
||||
{
|
||||
"author": "shinich39",
|
||||
"title": "comfyui-event-handler [USAFE/REMOVED]",
|
||||
"reference": "https://github.com/shinich39/comfyui-event-handler",
|
||||
"files": [
|
||||
"https://github.com/shinich39/comfyui-event-handler"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Javascript code will run when an event fires. [w/This node allows you to execute arbitrary JavaScript code as input for the workflow.]"
|
||||
},
|
||||
{
|
||||
"author": "Moooonet",
|
||||
"title": "ComfyUI-ArteMoon [REMOVED]",
|
||||
"reference": "https://github.com/Moooonet/ComfyUI-ArteMoon",
|
||||
"files": [
|
||||
"https://github.com/Moooonet/ComfyUI-ArteMoon"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This plugin works with [a/IF_AI_Tools](https://github.com/if-ai/ComfyUI-IF_AI_tools) to build a workflow in ComfyUI that uses AI to assist in generating prompts."
|
||||
},
|
||||
{
|
||||
"author": "ryanontheinside",
|
||||
"title": "ComfyUI-MediaPipe-Vision [REMOVED]",
|
||||
"reference": "https://github.com/ryanontheinside/ComfyUI-MediaPipe-Vision",
|
||||
"files": [
|
||||
"https://github.com/ryanontheinside/ComfyUI-MediaPipe-Vision"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A centralized wrapper of all MediaPipe vision tasks for ComfyUI."
|
||||
},
|
||||
{
|
||||
"author": "shinich39",
|
||||
"title": "comfyui-textarea-command [REMOVED]",
|
||||
"reference": "https://github.com/shinich39/comfyui-textarea-command",
|
||||
"files": [
|
||||
"https://github.com/shinich39/comfyui-textarea-command"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Add command and comment in textarea. (e.g. // Disabled line)"
|
||||
},
|
||||
{
|
||||
"author": "shinich39",
|
||||
"title": "comfyui-parse-image [REMOVED]",
|
||||
"reference": "https://github.com/shinich39/comfyui-parse-image",
|
||||
"files": [
|
||||
"https://github.com/shinich39/comfyui-parse-image"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Extract metadata from image."
|
||||
},
|
||||
{
|
||||
"author": "shinich39",
|
||||
"title": "comfyui-put-image [REMOVED]",
|
||||
"reference": "https://github.com/shinich39/comfyui-put-image",
|
||||
"files": [
|
||||
"https://github.com/shinich39/comfyui-put-image"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Load image from directory."
|
||||
},
|
||||
{
|
||||
"author": "fredconex",
|
||||
"title": "TripoSG Nodes for ComfyUI [REMOVED]",
|
||||
"reference": "https://github.com/fredconex/ComfyUI-TripoSG",
|
||||
"files": [
|
||||
"https://github.com/fredconex/ComfyUI-TripoSG"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Created by Alfredo Fernandes inspired by Hunyuan3D nodes by Kijai. This extension adds TripoSG 3D mesh generation capabilities to ComfyUI, allowing you to generate 3D meshes from a single image using the TripoSG model."
|
||||
},
|
||||
{
|
||||
"author": "fredconex",
|
||||
"title": "ComfyUI-PaintTurbo [REMOVED]",
|
||||
"reference": "https://github.com/fredconex/ComfyUI-PaintTurbo",
|
||||
"files": [
|
||||
"https://github.com/fredconex/ComfyUI-PaintTurbo"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "NODES: Hunyuan3D Texture Mesh"
|
||||
},
|
||||
{
|
||||
"author": "zhuanqianfish",
|
||||
"title": "TaesdDecoder [REMOVED]",
|
||||
"reference": "https://github.com/zhuanqianfish/TaesdDecoder",
|
||||
"files": [
|
||||
"https://github.com/zhuanqianfish/TaesdDecoder"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "use TAESD decoded image.you need donwload taesd_decoder.pth and taesdxl_decoder.pth to vae_approx folder first.\n It will result in a slight loss of image quality and a significant decrease in peak video memory during decoding."
|
||||
},
|
||||
{
|
||||
"author": "myAiLemon",
|
||||
"title": "MagicAutomaticPicture [REMOVED]",
|
||||
"reference": "https://github.com/myAiLemon/MagicAutomaticPicture",
|
||||
"files": [
|
||||
"https://github.com/myAiLemon/MagicAutomaticPicture"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A comfyui node package that can generate pictures and automatically save positive prompts and eliminate unwanted prompts"
|
||||
},
|
||||
{
|
||||
"author": "thisiseddy-ab",
|
||||
"title": "ComfyUI-Edins-Ultimate-Pack [REMOVED]",
|
||||
"reference": "https://github.com/thisiseddy-ab/ComfyUI-Edins-Ultimate-Pack",
|
||||
"files": [
|
||||
"https://github.com/thisiseddy-ab/ComfyUI-Edins-Ultimate-Pack"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Well i needet a Tiled Ksampler that still works for Comfy UI there were none so i made one, in this Package i will put all Nodes i will develop for Comfy Ui still in beta alot will change.."
|
||||
},
|
||||
{
|
||||
"author": "Davros666",
|
||||
"title": "safetriggers [REMOVED]",
|
||||
"reference": "https://github.com/Davros666/safetriggers",
|
||||
"files": [
|
||||
"https://github.com/Davros666/safetriggers"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI Nodes for READING TRIGGERS, TRIGGER-WORDS, TRIGGER-PHRASES FROM LoRAs"
|
||||
},
|
||||
{
|
||||
"author": "cubiq",
|
||||
"title": "Simple Math [REMOVED]",
|
||||
"id": "simplemath",
|
||||
"reference": "https://github.com/cubiq/ComfyUI_SimpleMath",
|
||||
"files": [
|
||||
"https://github.com/cubiq/ComfyUI_SimpleMath"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "custom node for ComfyUI to perform simple math operations"
|
||||
},
|
||||
{
|
||||
"author": "lucafoscili",
|
||||
"title": "LF Nodes [DEPRECATED]",
|
||||
"reference": "https://github.com/lucafoscili/comfyui-lf",
|
||||
"files": [
|
||||
"https://github.com/lucafoscili/comfyui-lf"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Custom nodes with a touch of extra UX, including: history for primitives, JSON manipulation, logic switches with visual feedback, LLM chat... and more!"
|
||||
},
|
||||
{
|
||||
"author": "AI2lab",
|
||||
"title": "comfyUI-tool-2lab [REMOVED]",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,106 @@
|
||||
{
|
||||
"models": [
|
||||
{
|
||||
"name": "Latent Bridge Matching for Image Relighting",
|
||||
"type": "diffusion_model",
|
||||
"base": "LBM",
|
||||
"save_path": "diffusion_models/LBM",
|
||||
"description": "Latent Bridge Matching (LBM) Relighting model",
|
||||
"reference": "https://huggingface.co/jasperai/LBM_relighting",
|
||||
"filename": "LBM_relighting.safetensors",
|
||||
"url": "https://huggingface.co/jasperai/LBM_relighting/resolve/main/model.safetensors",
|
||||
"size": "5.02GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "LTX-Video 13B Distilled v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Distilled version of the LTX-Video 13B model, providing improved efficiency while maintaining high-resolution quality.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-distilled.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B Distilled FP8 v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Quantized distilled version of the LTX-Video 13B model, optimized for even lower VRAM usage while maintaining quality.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-distilled-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-fp8.safetensors",
|
||||
"size": "15.7GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B Distilled LoRA v0.9.7",
|
||||
"type": "lora",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "loras",
|
||||
"description": "A LoRA adapter that transforms the standard LTX-Video 13B model into a distilled version when loaded.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-distilled-lora128.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-lora128.safetensors",
|
||||
"size": "1.33GB"
|
||||
},
|
||||
{
|
||||
"name": "lllyasviel/FramePackI2V_HY",
|
||||
"type": "FramePackI2V",
|
||||
"base": "FramePackI2V",
|
||||
"save_path": "diffusers/lllyasviel",
|
||||
"description": "[SNAPSHOT] This is the f1k1_x_g9_f1k1f2k2f16k4_td FramePack for HY. [w/You cannot download this item on ComfyUI-Manager versions below V3.18]",
|
||||
"reference": "https://huggingface.co/lllyasviel/FramePackI2V_HY",
|
||||
"filename": "<huggingface>",
|
||||
"url": "lllyasviel/FramePackI2V_HY",
|
||||
"size": "25.75GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "LTX-Video Spatial Upscaler v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Spatial upscaler model for LTX-Video. This model enhances the spatial resolution of generated videos.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-spatial-upscaler-0.9.7.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-spatial-upscaler-0.9.7.safetensors",
|
||||
"size": "505MB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video Temporal Upscaler v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Temporal upscaler model for LTX-Video. This model enhances the temporal resolution and smoothness of generated videos.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-temporal-upscaler-0.9.7.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-temporal-upscaler-0.9.7.safetensors",
|
||||
"size": "524MB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "High-resolution quality LTX-Video 13B model.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-dev.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-dev.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B FP8 v0.9.7",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Quantized version of the LTX-Video 13B model, optimized for lower VRAM usage while maintaining high quality.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.7-dev-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-dev-fp8.safetensors",
|
||||
"size": "15.7GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.1 i2v 480p 14B (bf16)",
|
||||
"type": "diffusion_model",
|
||||
@@ -590,121 +691,6 @@
|
||||
"filename": "sigclip_vision_patch14_384.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors",
|
||||
"size": "857MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "comfyanonymous/flux_text_encoders - t5xxl (fp16)",
|
||||
"type": "clip",
|
||||
"base": "t5",
|
||||
"save_path": "text_encoders/t5",
|
||||
"description": "Text Encoders for FLUX (fp16)",
|
||||
"reference": "https://huggingface.co/comfyanonymous/flux_text_encoders",
|
||||
"filename": "t5xxl_fp16.safetensors",
|
||||
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors",
|
||||
"size": "9.79GB"
|
||||
},
|
||||
{
|
||||
"name": "comfyanonymous/flux_text_encoders - t5xxl (fp8_e4m3fn)",
|
||||
"type": "clip",
|
||||
"base": "t5",
|
||||
"save_path": "text_encoders/t5",
|
||||
"description": "Text Encoders for FLUX (fp8_e4m3fn)",
|
||||
"reference": "https://huggingface.co/comfyanonymous/flux_text_encoders",
|
||||
"filename": "t5xxl_fp8_e4m3fn.safetensors",
|
||||
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn.safetensors",
|
||||
"size": "4.89GB"
|
||||
},
|
||||
{
|
||||
"name": "comfyanonymous/flux_text_encoders - t5xxl (fp8_e4m3fn_scaled)",
|
||||
"type": "clip",
|
||||
"base": "t5",
|
||||
"save_path": "text_encoders/t5",
|
||||
"description": "Text Encoders for FLUX (fp16)",
|
||||
"reference": "https://huggingface.co/comfyanonymous/flux_text_encoders",
|
||||
"filename": "t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"size": "5.16GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "FLUX.1 [Dev] Diffusion model (scaled fp8)",
|
||||
"type": "diffusion_model",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "diffusion_models/FLUX1",
|
||||
"description": "FLUX.1 [Dev] Diffusion model (scaled fp8)[w/Due to the large size of the model, it is recommended to download it through a browser if possible.]",
|
||||
"reference": "https://huggingface.co/comfyanonymous/flux_dev_scaled_fp8_test",
|
||||
"filename": "flux_dev_fp8_scaled_diffusion_model.safetensors",
|
||||
"url": "https://huggingface.co/comfyanonymous/flux_dev_scaled_fp8_test/resolve/main/flux_dev_fp8_scaled_diffusion_model.safetensors",
|
||||
"size": "11.9GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "kijai/MoGe_ViT_L_fp16.safetensors",
|
||||
"type": "MoGe",
|
||||
"base": "MoGe",
|
||||
"save_path": "MoGe",
|
||||
"description": "Safetensors versions of [a/https://github.com/microsoft/MoGe](https://github.com/microsoft/MoGe)",
|
||||
"reference": "https://huggingface.co/Kijai/MoGe_safetensors",
|
||||
"filename": "MoGe_ViT_L_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Kijai/MoGe_safetensors/resolve/main/MoGe_ViT_L_fp16.safetensors",
|
||||
"size": "628MB"
|
||||
},
|
||||
{
|
||||
"name": "kijai/MoGe_ViT_L_fp16.safetensors",
|
||||
"type": "MoGe",
|
||||
"base": "MoGe",
|
||||
"save_path": "MoGe",
|
||||
"description": "Safetensors versions of [a/https://github.com/microsoft/MoGe](https://github.com/microsoft/MoGe)",
|
||||
"reference": "https://huggingface.co/Kijai/MoGe_safetensors",
|
||||
"filename": "MoGe_ViT_L_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Kijai/MoGe_safetensors/resolve/main/MoGe_ViT_L_fp16.safetensors",
|
||||
"size": "1.26GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "pulid_flux_v0.9.1.safetensors",
|
||||
"type": "PuLID",
|
||||
"base": "FLUX",
|
||||
"save_path": "pulid",
|
||||
"description": "This is required for PuLID (FLUX)",
|
||||
"reference": "https://huggingface.co/guozinan/PuLID",
|
||||
"filename": "pulid_flux_v0.9.1.safetensors",
|
||||
"url": "https://huggingface.co/guozinan/PuLID/resolve/main/pulid_flux_v0.9.1.safetensors",
|
||||
"size": "1.14GB"
|
||||
},
|
||||
{
|
||||
"name": "pulid_v1.1.safetensors",
|
||||
"type": "PuLID",
|
||||
"base": "SDXL",
|
||||
"save_path": "pulid",
|
||||
"description": "This is required for PuLID (SDXL)",
|
||||
"reference": "https://huggingface.co/guozinan/PuLID",
|
||||
"filename": "pulid_v1.1.safetensors",
|
||||
"url": "https://huggingface.co/guozinan/PuLID/resolve/main/pulid_v1.1.safetensors",
|
||||
"size": "984MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Kolors-IP-Adapter-Plus.bin (Kwai-Kolors/Kolors-IP-Adapter-Plus)",
|
||||
"type": "IP-Adapter",
|
||||
"base": "Kolors",
|
||||
"save_path": "ipadapter",
|
||||
"description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
|
||||
"reference": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus",
|
||||
"filename": "Kolors-IP-Adapter-Plus.bin",
|
||||
"url": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus/resolve/main/ip_adapter_plus_general.bin",
|
||||
"size": "1.01GB"
|
||||
},
|
||||
{
|
||||
"name": "Kolors-IP-Adapter-FaceID-Plus.bin (Kwai-Kolors/Kolors-IP-Adapter-Plus)",
|
||||
"type": "IP-Adapter",
|
||||
"base": "Kolors",
|
||||
"save_path": "ipadapter",
|
||||
"description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
|
||||
"reference": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-FaceID-Plus",
|
||||
"filename": "Kolors-IP-Adapter-FaceID-Plus.bin",
|
||||
"url": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-FaceID-Plus/resolve/main/ipa-faceid-plus.bin",
|
||||
"size": "2.39GB"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "Comfy-Org",
|
||||
"title": "ComfyUI React Extension Template",
|
||||
"reference": "https://github.com/Comfy-Org/ComfyUI-React-Extension-Template",
|
||||
"files": [
|
||||
"https://github.com/Comfy-Org/ComfyUI-React-Extension-Template"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A minimal template for creating React/TypeScript frontend extensions for ComfyUI, with complete boilerplate setup including internationalization and unit testing."
|
||||
},
|
||||
{
|
||||
"author": "Suzie1",
|
||||
"title": "Guide To Making Custom Nodes in ComfyUI",
|
||||
|
||||
373
notebooks/comfyui_colab_with_manager.ipynb
Normal file
373
notebooks/comfyui_colab_with_manager.ipynb
Normal file
@@ -0,0 +1,373 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "aaaaaaaaaa"
|
||||
},
|
||||
"source": [
|
||||
"Git clone the repo and install the requirements. (ignore the pip errors about protobuf)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "bbbbbbbbbb"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# #@title Environment Setup\n",
|
||||
"\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"OPTIONS = {}\n",
|
||||
"\n",
|
||||
"USE_GOOGLE_DRIVE = True #@param {type:\"boolean\"}\n",
|
||||
"UPDATE_COMFY_UI = True #@param {type:\"boolean\"}\n",
|
||||
"USE_COMFYUI_MANAGER = True #@param {type:\"boolean\"}\n",
|
||||
"INSTALL_CUSTOM_NODES_DEPENDENCIES = True #@param {type:\"boolean\"}\n",
|
||||
"OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n",
|
||||
"OPTIONS['UPDATE_COMFY_UI'] = UPDATE_COMFY_UI\n",
|
||||
"OPTIONS['USE_COMFYUI_MANAGER'] = USE_COMFYUI_MANAGER\n",
|
||||
"OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES'] = INSTALL_CUSTOM_NODES_DEPENDENCIES\n",
|
||||
"\n",
|
||||
"current_dir = !pwd\n",
|
||||
"WORKSPACE = f\"{current_dir[0]}/ComfyUI\"\n",
|
||||
"\n",
|
||||
"if OPTIONS['USE_GOOGLE_DRIVE']:\n",
|
||||
" !echo \"Mounting Google Drive...\"\n",
|
||||
" %cd /\n",
|
||||
"\n",
|
||||
" from google.colab import drive\n",
|
||||
" drive.mount('/content/drive')\n",
|
||||
"\n",
|
||||
" WORKSPACE = \"/content/drive/MyDrive/ComfyUI\"\n",
|
||||
" %cd /content/drive/MyDrive\n",
|
||||
"\n",
|
||||
"![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
|
||||
"%cd $WORKSPACE\n",
|
||||
"\n",
|
||||
"if OPTIONS['UPDATE_COMFY_UI']:\n",
|
||||
" !echo -= Updating ComfyUI =-\n",
|
||||
"\n",
|
||||
" # Correction of the issue of permissions being deleted on Google Drive.\n",
|
||||
" ![ -f \".ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\n",
|
||||
" ![ -f \".ci/nightly/windows_base_files/run_nvidia_gpu.bat\" ] && chmod 755 .ci/nightly/windows_base_files/run_nvidia_gpu.bat\n",
|
||||
" ![ -f \".ci/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows/update_comfyui_and_python_dependencies.bat\n",
|
||||
" ![ -f \".ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\n",
|
||||
" ![ -f \".ci/update_windows/update.py\" ] && chmod 755 .ci/update_windows/update.py\n",
|
||||
" ![ -f \".ci/update_windows/update_comfyui.bat\" ] && chmod 755 .ci/update_windows/update_comfyui.bat\n",
|
||||
" ![ -f \".ci/update_windows/README_VERY_IMPORTANT.txt\" ] && chmod 755 .ci/update_windows/README_VERY_IMPORTANT.txt\n",
|
||||
" ![ -f \".ci/update_windows/run_cpu.bat\" ] && chmod 755 .ci/update_windows/run_cpu.bat\n",
|
||||
" ![ -f \".ci/update_windows/run_nvidia_gpu.bat\" ] && chmod 755 .ci/update_windows/run_nvidia_gpu.bat\n",
|
||||
"\n",
|
||||
" !git pull\n",
|
||||
"\n",
|
||||
"!echo -= Install dependencies =-\n",
|
||||
"!pip3 install accelerate\n",
|
||||
"!pip3 install einops transformers>=4.28.1 safetensors>=0.4.2 aiohttp pyyaml Pillow scipy tqdm psutil tokenizers>=0.13.3\n",
|
||||
"!pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121\n",
|
||||
"!pip3 install torchsde\n",
|
||||
"!pip3 install kornia>=0.7.1 spandrel soundfile sentencepiece\n",
|
||||
"\n",
|
||||
"if OPTIONS['USE_COMFYUI_MANAGER']:\n",
|
||||
" %cd custom_nodes\n",
|
||||
"\n",
|
||||
" # Correction of the issue of permissions being deleted on Google Drive.\n",
|
||||
" ![ -f \"ComfyUI-Manager/check.sh\" ] && chmod 755 ComfyUI-Manager/check.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/scan.sh\" ] && chmod 755 ComfyUI-Manager/scan.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/node_db/dev/scan.sh\" ] && chmod 755 ComfyUI-Manager/node_db/dev/scan.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/node_db/tutorial/scan.sh\" ] && chmod 755 ComfyUI-Manager/node_db/tutorial/scan.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\n",
|
||||
"\n",
|
||||
" ![ ! -d ComfyUI-Manager ] && echo -= Initial setup ComfyUI-Manager =- && git clone https://github.com/ltdrdata/ComfyUI-Manager\n",
|
||||
" %cd ComfyUI-Manager\n",
|
||||
" !git pull\n",
|
||||
"\n",
|
||||
"%cd $WORKSPACE\n",
|
||||
"\n",
|
||||
"if OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES']:\n",
|
||||
" !echo -= Install custom nodes dependencies =-\n",
|
||||
" !pip install GitPython\n",
|
||||
" !python custom_nodes/ComfyUI-Manager/cm-cli.py restore-dependencies\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "cccccccccc"
|
||||
},
|
||||
"source": [
|
||||
"Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "dddddddddd"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Checkpoints\n",
|
||||
"\n",
|
||||
"### SDXL\n",
|
||||
"### I recommend these workflow examples: https://comfyanonymous.github.io/ComfyUI_examples/sdxl/\n",
|
||||
"\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# SDXL ReVision\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors -P ./models/clip_vision/\n",
|
||||
"\n",
|
||||
"# SD1.5\n",
|
||||
"!wget -c https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# SD2\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# Some SD1.5 anime style\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1_orangemixs.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A3_orangemixs.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/Linaqruf/anything-v3.0/resolve/main/anything-v3-fp16-pruned.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# Waifu Diffusion 1.5 (anime style SD2.x 768-v)\n",
|
||||
"#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta3/resolve/main/wd-illusion-fp16.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# unCLIP models\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# VAE\n",
|
||||
"!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n",
|
||||
"#!wget -c https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt -P ./models/vae/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Loras\n",
|
||||
"#!wget -c https://civitai.com/api/download/models/10350 -O ./models/loras/theovercomer8sContrastFix_sd21768.safetensors #theovercomer8sContrastFix SD2.x 768-v\n",
|
||||
"#!wget -c https://civitai.com/api/download/models/10638 -O ./models/loras/theovercomer8sContrastFix_sd15.safetensors #theovercomer8sContrastFix SD1.x\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors -P ./models/loras/ #SDXL offset noise lora\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# T2I-Adapter\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_seg_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_sketch_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_keypose_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_openpose_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_color_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_canny_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"\n",
|
||||
"# T2I Styles Model\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_style_sd14v1.pth -P ./models/style_models/\n",
|
||||
"\n",
|
||||
"# CLIPVision model (needed for styles model)\n",
|
||||
"#!wget -c https://huggingface.co/openai/clip-vit-large-patch14/resolve/main/pytorch_model.bin -O ./models/clip_vision/clip_vit14.bin\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# ControlNet\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11f1p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"\n",
|
||||
"# ControlNet SDXL\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-canny-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-depth-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-recolor-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"\n",
|
||||
"# Controlnet Preprocessor nodes by Fannovel16\n",
|
||||
"#!cd custom_nodes && git clone https://github.com/Fannovel16/comfy_controlnet_preprocessors; cd comfy_controlnet_preprocessors && python install.py\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# GLIGEN\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/GLIGEN_pruned_safetensors/resolve/main/gligen_sd14_textbox_pruned_fp16.safetensors -P ./models/gligen/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# ESRGAN upscale model\n",
|
||||
"#!wget -c https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P ./models/upscale_models/\n",
|
||||
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x2.pth -P ./models/upscale_models/\n",
|
||||
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "kkkkkkkkkkkkkkk"
|
||||
},
|
||||
"source": [
|
||||
"### Run ComfyUI with cloudflared (Recommended Way)\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "jjjjjjjjjjjjjj"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!wget -P ~ https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n",
|
||||
"!dpkg -i ~/cloudflared-linux-amd64.deb\n",
|
||||
"\n",
|
||||
"import subprocess\n",
|
||||
"import threading\n",
|
||||
"import time\n",
|
||||
"import socket\n",
|
||||
"import urllib.request\n",
|
||||
"\n",
|
||||
"def iframe_thread(port):\n",
|
||||
" while True:\n",
|
||||
" time.sleep(0.5)\n",
|
||||
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" result = sock.connect_ex(('127.0.0.1', port))\n",
|
||||
" if result == 0:\n",
|
||||
" break\n",
|
||||
" sock.close()\n",
|
||||
" print(\"\\nComfyUI finished loading, trying to launch cloudflared (if it gets stuck here cloudflared is having issues)\\n\")\n",
|
||||
"\n",
|
||||
" p = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:{}\".format(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
|
||||
" for line in p.stderr:\n",
|
||||
" l = line.decode()\n",
|
||||
" if \"trycloudflare.com \" in l:\n",
|
||||
" print(\"This is the URL to access ComfyUI:\", l[l.find(\"http\"):], end='')\n",
|
||||
" #print(l, end='')\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
|
||||
"\n",
|
||||
"!python main.py --dont-print-server"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "kkkkkkkkkkkkkk"
|
||||
},
|
||||
"source": [
|
||||
"### Run ComfyUI with localtunnel\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "jjjjjjjjjjjjj"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!npm install -g localtunnel\n",
|
||||
"\n",
|
||||
"import subprocess\n",
|
||||
"import threading\n",
|
||||
"import time\n",
|
||||
"import socket\n",
|
||||
"import urllib.request\n",
|
||||
"\n",
|
||||
"def iframe_thread(port):\n",
|
||||
" while True:\n",
|
||||
" time.sleep(0.5)\n",
|
||||
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" result = sock.connect_ex(('127.0.0.1', port))\n",
|
||||
" if result == 0:\n",
|
||||
" break\n",
|
||||
" sock.close()\n",
|
||||
" print(\"\\nComfyUI finished loading, trying to launch localtunnel (if it gets stuck here localtunnel is having issues)\\n\")\n",
|
||||
"\n",
|
||||
" print(\"The password/enpoint ip for localtunnel is:\", urllib.request.urlopen('https://ipv4.icanhazip.com').read().decode('utf8').strip(\"\\n\"))\n",
|
||||
" p = subprocess.Popen([\"lt\", \"--port\", \"{}\".format(port)], stdout=subprocess.PIPE)\n",
|
||||
" for line in p.stdout:\n",
|
||||
" print(line.decode(), end='')\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
|
||||
"\n",
|
||||
"!python main.py --dont-print-server"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "gggggggggg"
|
||||
},
|
||||
"source": [
|
||||
"### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n",
|
||||
"\n",
|
||||
"You should see the ui appear in an iframe. If you get a 403 error, it's your firefox settings or an extension that's messing things up.\n",
|
||||
"\n",
|
||||
"If you want to open it in another window use the link.\n",
|
||||
"\n",
|
||||
"Note that some UI features like live image previews won't work because the colab iframe blocks websockets."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "hhhhhhhhhh"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import threading\n",
|
||||
"import time\n",
|
||||
"import socket\n",
|
||||
"def iframe_thread(port):\n",
|
||||
" while True:\n",
|
||||
" time.sleep(0.5)\n",
|
||||
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" result = sock.connect_ex(('127.0.0.1', port))\n",
|
||||
" if result == 0:\n",
|
||||
" break\n",
|
||||
" sock.close()\n",
|
||||
" from google.colab import output\n",
|
||||
" output.serve_kernel_port_as_iframe(port, height=1024)\n",
|
||||
" print(\"to open it in a window you can open this link here:\")\n",
|
||||
" output.serve_kernel_port_as_window(port)\n",
|
||||
"\n",
|
||||
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
|
||||
"\n",
|
||||
"!python main.py --dont-print-server"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"gpuClass": "standard",
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
@@ -12,10 +12,13 @@ import ast
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from .glob import security_check
|
||||
from .glob import manager_util
|
||||
from .glob import cm_global
|
||||
from .glob import manager_downloader
|
||||
glob_path = os.path.join(os.path.dirname(__file__), "glob")
|
||||
sys.path.append(glob_path)
|
||||
|
||||
import security_check
|
||||
import manager_util
|
||||
import cm_global
|
||||
import manager_downloader
|
||||
import folder_paths
|
||||
|
||||
manager_util.add_python_path_to_env()
|
||||
@@ -37,8 +40,8 @@ else:
|
||||
|
||||
security_check.security_check()
|
||||
|
||||
cm_global.pip_blacklist = {'torch', 'torchsde', 'torchvision'}
|
||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
|
||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||
|
||||
|
||||
def skip_pip_spam(x):
|
||||
@@ -65,17 +68,15 @@ comfy_path = os.environ.get('COMFYUI_PATH')
|
||||
comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
|
||||
|
||||
if comfy_path is None:
|
||||
try:
|
||||
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
|
||||
os.environ['COMFYUI_PATH'] = comfy_path
|
||||
except:
|
||||
print("[ComfyUI-Manager] environment variable 'COMFYUI_PATH' is not specified.")
|
||||
exit(-1)
|
||||
# legacy env var
|
||||
comfy_path = os.environ.get('COMFYUI_PATH')
|
||||
|
||||
if comfy_path is None:
|
||||
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
|
||||
|
||||
if comfy_base_path is None:
|
||||
comfy_base_path = comfy_path
|
||||
|
||||
|
||||
sys.__comfyui_manager_register_message_collapse = register_message_collapse
|
||||
sys.__comfyui_manager_is_import_failed_extension = is_import_failed_extension
|
||||
cm_global.register_api('cm.register_message_collapse', register_message_collapse)
|
||||
@@ -120,11 +121,17 @@ read_config()
|
||||
read_uv_mode()
|
||||
check_file_logging()
|
||||
|
||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||
if sys.version_info < (3, 13):
|
||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||
else:
|
||||
cm_global.pip_overrides = {}
|
||||
|
||||
if os.path.exists(manager_pip_overrides_path):
|
||||
with open(manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
cm_global.pip_overrides = json.load(json_file)
|
||||
cm_global.pip_overrides['numpy'] = 'numpy<2'
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
cm_global.pip_overrides['numpy'] = 'numpy<2'
|
||||
|
||||
|
||||
if os.path.exists(manager_pip_blacklist_path):
|
||||
@@ -391,11 +398,7 @@ try:
|
||||
def emit(self, record):
|
||||
global is_start_mode
|
||||
|
||||
try:
|
||||
message = record.getMessage()
|
||||
except Exception as e:
|
||||
message = f"<<logging error>>: {record} - {e}"
|
||||
original_stderr.write(message)
|
||||
message = record.getMessage()
|
||||
|
||||
if is_start_mode:
|
||||
match = re.search(pat_import_fail, message)
|
||||
@@ -623,6 +626,7 @@ def execute_lazy_install_script(repo_path, executable):
|
||||
lines = manager_util.robust_readlines(requirements_path)
|
||||
for line in lines:
|
||||
package_name = remap_pip_package(line.strip())
|
||||
package_name = package_name.split('#')[0].strip()
|
||||
if package_name and not is_installed(package_name):
|
||||
if '--index-url' in package_name:
|
||||
s = package_name.split('--index-url')
|
||||
@@ -1,62 +1,15 @@
|
||||
[build-system]
|
||||
requires = ["setuptools >= 61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "comfyui-manager"
|
||||
license = { text = "GPL-3.0-only" }
|
||||
version = "4.0.0-beta.1"
|
||||
requires-python = ">= 3.9"
|
||||
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
|
||||
readme = "README.md"
|
||||
keywords = ["comfyui", "comfyui-manager"]
|
||||
|
||||
maintainers = [
|
||||
{ name = "Dr.Lt.Data", email = "dr.lt.data@gmail.com" },
|
||||
{ name = "Yoland Yan", email = "yoland@drip.art" },
|
||||
{ name = "James Kwon", email = "hongilkwon316@gmail.com" },
|
||||
{ name = "Robin Huang", email = "robin@drip.art" },
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"GitPython",
|
||||
"PyGithub",
|
||||
"matrix-client==0.4.0",
|
||||
"transformers",
|
||||
"huggingface-hub>0.20",
|
||||
"typer",
|
||||
"rich",
|
||||
"typing-extensions",
|
||||
"toml",
|
||||
"uv",
|
||||
"chardet"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pre-commit", "pytest", "ruff", "pytest-cov"]
|
||||
version = "3.32.3"
|
||||
license = { file = "LICENSE.txt" }
|
||||
dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/ltdrdata/ComfyUI-Manager"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["comfyui_manager*"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py39"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E4", # default
|
||||
"E7", # default
|
||||
"E9", # default
|
||||
"F", # default
|
||||
"I", # isort-like behavior (import statement sorting)
|
||||
]
|
||||
[tool.comfy]
|
||||
PublisherId = "drltdata"
|
||||
DisplayName = "ComfyUI-Manager"
|
||||
Icon = ""
|
||||
|
||||
39
scripts/colab-dependencies.py
Normal file
39
scripts/colab-dependencies.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
def get_enabled_subdirectories_with_files(base_directory):
|
||||
subdirs_with_files = []
|
||||
for subdir in os.listdir(base_directory):
|
||||
try:
|
||||
full_path = os.path.join(base_directory, subdir)
|
||||
if os.path.isdir(full_path) and not subdir.endswith(".disabled") and not subdir.startswith('.') and subdir != '__pycache__':
|
||||
print(f"## Install dependencies for '{subdir}'")
|
||||
requirements_file = os.path.join(full_path, "requirements.txt")
|
||||
install_script = os.path.join(full_path, "install.py")
|
||||
|
||||
if os.path.exists(requirements_file) or os.path.exists(install_script):
|
||||
subdirs_with_files.append((full_path, requirements_file, install_script))
|
||||
except Exception as e:
|
||||
print(f"EXCEPTION During Dependencies INSTALL on '{subdir}':\n{e}")
|
||||
|
||||
return subdirs_with_files
|
||||
|
||||
|
||||
def install_requirements(requirements_file_path):
|
||||
if os.path.exists(requirements_file_path):
|
||||
subprocess.run(["pip", "install", "-r", requirements_file_path])
|
||||
|
||||
|
||||
def run_install_script(install_script_path):
|
||||
if os.path.exists(install_script_path):
|
||||
subprocess.run(["python", install_script_path])
|
||||
|
||||
|
||||
custom_nodes_directory = "custom_nodes"
|
||||
subdirs_with_files = get_enabled_subdirectories_with_files(custom_nodes_directory)
|
||||
|
||||
|
||||
for subdir, requirements_file, install_script in subdirs_with_files:
|
||||
install_requirements(requirements_file)
|
||||
run_install_script(install_script)
|
||||
21
scripts/install-comfyui-venv-linux.sh
Executable file
21
scripts/install-comfyui-venv-linux.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
git clone https://github.com/comfyanonymous/ComfyUI
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
|
||||
cd ..
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
|
||||
cd ..
|
||||
echo "#!/bin/bash" > run_gpu.sh
|
||||
echo "cd ComfyUI" >> run_gpu.sh
|
||||
echo "source venv/bin/activate" >> run_gpu.sh
|
||||
echo "python main.py --preview-method auto" >> run_gpu.sh
|
||||
chmod +x run_gpu.sh
|
||||
|
||||
echo "#!/bin/bash" > run_cpu.sh
|
||||
echo "cd ComfyUI" >> run_cpu.sh
|
||||
echo "source venv/bin/activate" >> run_cpu.sh
|
||||
echo "python main.py --preview-method auto --cpu" >> run_cpu.sh
|
||||
chmod +x run_cpu.sh
|
||||
17
scripts/install-comfyui-venv-win.bat
Executable file
17
scripts/install-comfyui-venv-win.bat
Executable file
@@ -0,0 +1,17 @@
|
||||
git clone https://github.com/comfyanonymous/ComfyUI
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
|
||||
cd ..
|
||||
python -m venv venv
|
||||
call venv/Scripts/activate
|
||||
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
|
||||
cd ..
|
||||
echo "cd ComfyUI" >> run_gpu.bat
|
||||
echo "call venv/Scripts/activate" >> run_gpu.bat
|
||||
echo "python main.py" >> run_gpu.bat
|
||||
|
||||
echo "cd ComfyUI" >> run_cpu.bat
|
||||
echo "call venv/Scripts/activate" >> run_cpu.bat
|
||||
echo "python main.py --cpu" >> run_cpu.bat
|
||||
3
scripts/install-manager-for-portable-version.bat
Normal file
3
scripts/install-manager-for-portable-version.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
.\python_embeded\python.exe -s -m pip install gitpython
|
||||
.\python_embeded\python.exe -c "import git; git.Repo.clone_from('https://github.com/ltdrdata/ComfyUI-Manager', './ComfyUI/custom_nodes/comfyui-manager')"
|
||||
.\python_embeded\python.exe -m pip install -r ./ComfyUI/custom_nodes/comfyui-manager/requirements.txt
|
||||
0
snapshots/the_snapshot_files_are_located_here
Normal file
0
snapshots/the_snapshot_files_are_located_here
Normal file
19
tests-api/.gitignore
vendored
Normal file
19
tests-api/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Python cache files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Pytest cache
|
||||
.pytest_cache/
|
||||
|
||||
# Coverage reports
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Test-specific resources
|
||||
resources/tmp/
|
||||
91
tests-api/README.md
Normal file
91
tests-api/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# ComfyUI-Manager API Tests
|
||||
|
||||
This directory contains tests for the ComfyUI-Manager API endpoints, validating the OpenAPI specification and ensuring API functionality.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install test dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements-test.txt
|
||||
```
|
||||
|
||||
2. Ensure ComfyUI is running with ComfyUI-Manager installed:
|
||||
|
||||
```bash
|
||||
# Start ComfyUI with the default server
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run all tests
|
||||
|
||||
```bash
|
||||
pytest -xvs
|
||||
```
|
||||
|
||||
### Run specific test files
|
||||
|
||||
```bash
|
||||
# Run only the spec validation tests
|
||||
pytest -xvs test_spec_validation.py
|
||||
|
||||
# Run only the custom node API tests
|
||||
pytest -xvs test_customnode_api.py
|
||||
```
|
||||
|
||||
### Run specific test functions
|
||||
|
||||
```bash
|
||||
# Run a specific test
|
||||
pytest -xvs test_customnode_api.py::test_get_custom_node_list
|
||||
```
|
||||
|
||||
## Test Configuration
|
||||
|
||||
The tests use the following default configuration:
|
||||
|
||||
- Server URL: `http://localhost:8188`
|
||||
- Server timeout: 2 seconds
|
||||
- Wait between requests: 0.5 seconds
|
||||
- Maximum retries: 3
|
||||
|
||||
You can override these settings with environment variables:
|
||||
|
||||
```bash
|
||||
# Use a different server URL
|
||||
COMFYUI_SERVER_URL=http://localhost:8189 pytest -xvs
|
||||
```
|
||||
|
||||
## Test Categories
|
||||
|
||||
The tests are organized into the following categories:
|
||||
|
||||
1. **Spec Validation** (`test_spec_validation.py`): Validates that the OpenAPI specification is correct and complete.
|
||||
2. **Custom Node API** (`test_customnode_api.py`): Tests for custom node management endpoints.
|
||||
3. **Snapshot API** (`test_snapshot_api.py`): Tests for snapshot management endpoints.
|
||||
4. **Queue API** (`test_queue_api.py`): Tests for queue management endpoints.
|
||||
5. **Config API** (`test_config_api.py`): Tests for configuration endpoints.
|
||||
6. **Model API** (`test_model_api.py`): Tests for model management endpoints (minimal as these are being deprecated).
|
||||
|
||||
## Test Implementation Details
|
||||
|
||||
### Fixtures
|
||||
|
||||
- `test_config`: Provides the test configuration
|
||||
- `server_url`: Returns the server URL from the configuration
|
||||
- `openapi_spec`: Loads the OpenAPI specification
|
||||
- `api_client`: Creates a requests Session for API calls
|
||||
- `api_request`: Helper function for making consistent API requests
|
||||
|
||||
### Utilities
|
||||
|
||||
- `validation.py`: Functions for validating responses against the OpenAPI schema
|
||||
- `schema_utils.py`: Utilities for extracting and manipulating schemas
|
||||
|
||||
## Notes
|
||||
|
||||
- Some tests are skipped with `@pytest.mark.skip` to avoid modifying state in automated testing
|
||||
- Security-level restricted endpoints have minimal tests to avoid security issues
|
||||
- Tests focus on read operations rather than write operations where possible
|
||||
1
tests-api/__init__.py
Normal file
1
tests-api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Make tests-api directory a proper package
|
||||
237
tests-api/conftest.py
Normal file
237
tests-api/conftest.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
PyTest configuration and fixtures for API tests.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pytest
|
||||
import requests
|
||||
import tempfile
|
||||
import time
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Dict, Generator, Optional, Tuple
|
||||
|
||||
# Import test utilities
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Get the absolute path to the current file (conftest.py)
|
||||
current_file = Path(os.path.abspath(__file__))
|
||||
|
||||
# Get the directory containing the current file (the tests-api directory)
|
||||
tests_api_dir = current_file.parent
|
||||
|
||||
# Add the tests-api directory to the Python path
|
||||
if str(tests_api_dir) not in sys.path:
|
||||
sys.path.insert(0, str(tests_api_dir))
|
||||
|
||||
# Apply mocks for ComfyUI imports
|
||||
from mocks.patch import apply_mocks
|
||||
apply_mocks()
|
||||
|
||||
# Now we can import from utils.validation
|
||||
from utils.validation import load_openapi_spec
|
||||
|
||||
|
||||
# Default test configuration
|
||||
DEFAULT_TEST_CONFIG = {
|
||||
"server_url": "http://localhost:8188",
|
||||
"server_timeout": 2, # seconds
|
||||
"wait_between_requests": 0.5, # seconds
|
||||
"max_retries": 3,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_config() -> Dict:
|
||||
"""
|
||||
Load test configuration from environment variables or use defaults.
|
||||
"""
|
||||
config = DEFAULT_TEST_CONFIG.copy()
|
||||
|
||||
# Override from environment variables if present
|
||||
if "COMFYUI_SERVER_URL" in os.environ:
|
||||
config["server_url"] = os.environ["COMFYUI_SERVER_URL"]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def server_url(test_config: Dict) -> str:
|
||||
"""
|
||||
Get the server URL from the test configuration.
|
||||
"""
|
||||
return test_config["server_url"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def openapi_spec() -> Dict:
|
||||
"""
|
||||
Load the OpenAPI specification.
|
||||
"""
|
||||
return load_openapi_spec()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_client(server_url: str, test_config: Dict) -> requests.Session:
|
||||
"""
|
||||
Create a requests Session for API calls.
|
||||
"""
|
||||
session = requests.Session()
|
||||
|
||||
# Check if the server is running
|
||||
try:
|
||||
response = session.get(f"{server_url}/", timeout=test_config["server_timeout"])
|
||||
response.raise_for_status()
|
||||
except (requests.ConnectionError, requests.Timeout, requests.HTTPError):
|
||||
pytest.skip("ComfyUI server is not running or not accessible")
|
||||
|
||||
return session
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def temp_dir() -> Generator[Path, None, None]:
|
||||
"""
|
||||
Create a temporary directory for test files.
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
yield Path(temp_dir)
|
||||
|
||||
|
||||
class SecurityLevelContext:
|
||||
"""
|
||||
Context manager for setting and restoring security levels.
|
||||
"""
|
||||
def __init__(self, api_client: requests.Session, server_url: str, security_level: str):
|
||||
self.api_client = api_client
|
||||
self.server_url = server_url
|
||||
self.security_level = security_level
|
||||
self.original_level = None
|
||||
|
||||
async def __aenter__(self):
|
||||
# Get the current security level (not directly exposed in API, would require more setup)
|
||||
# For now, we'll just set the new level
|
||||
|
||||
# Set the new security level
|
||||
# Note: In a real implementation, we would need a way to set this
|
||||
# This is a placeholder - the actual implementation would depend on how
|
||||
# security levels are managed in ComfyUI-Manager
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
# Restore the original security level if needed
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def security_level_context(api_client: requests.Session, server_url: str):
|
||||
"""
|
||||
Create a context manager for setting security levels.
|
||||
"""
|
||||
return lambda level: SecurityLevelContext(api_client, server_url, level)
|
||||
|
||||
|
||||
def make_api_url(server_url: str, path: str) -> str:
|
||||
"""
|
||||
Construct a full API URL from the server URL and path.
|
||||
"""
|
||||
# Ensure the path starts with a slash
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
|
||||
# Remove trailing slash from server_url if present
|
||||
if server_url.endswith("/"):
|
||||
server_url = server_url[:-1]
|
||||
|
||||
return f"{server_url}{path}"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_request(api_client: requests.Session, server_url: str, test_config: Dict):
|
||||
"""
|
||||
Helper function for making API requests with consistent behavior.
|
||||
"""
|
||||
def _request(
|
||||
method: str,
|
||||
path: str,
|
||||
params: Optional[Dict] = None,
|
||||
json_data: Optional[Dict] = None,
|
||||
headers: Optional[Dict] = None,
|
||||
expected_status: int = 200,
|
||||
retry_on_error: bool = True,
|
||||
) -> Tuple[requests.Response, Optional[Dict]]:
|
||||
"""
|
||||
Make an API request with automatic validation.
|
||||
|
||||
Args:
|
||||
method: HTTP method
|
||||
path: API path
|
||||
params: Query parameters
|
||||
json_data: JSON request body
|
||||
headers: HTTP headers
|
||||
expected_status: Expected HTTP status code
|
||||
retry_on_error: Whether to retry on connection errors
|
||||
|
||||
Returns:
|
||||
Tuple of (Response object, JSON response data or None)
|
||||
"""
|
||||
method = method.lower()
|
||||
url = make_api_url(server_url, path)
|
||||
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
# Add common headers
|
||||
headers.setdefault("Accept", "application/json")
|
||||
|
||||
# Sleep between requests to avoid overwhelming the server
|
||||
time.sleep(test_config["wait_between_requests"])
|
||||
|
||||
retries = test_config["max_retries"] if retry_on_error else 0
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(retries + 1):
|
||||
try:
|
||||
if method == "get":
|
||||
response = api_client.get(url, params=params, headers=headers)
|
||||
elif method == "post":
|
||||
response = api_client.post(url, params=params, json=json_data, headers=headers)
|
||||
elif method == "put":
|
||||
response = api_client.put(url, params=params, json=json_data, headers=headers)
|
||||
elif method == "delete":
|
||||
response = api_client.delete(url, params=params, headers=headers)
|
||||
else:
|
||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
|
||||
# Check status code
|
||||
assert response.status_code == expected_status, (
|
||||
f"Expected status code {expected_status}, got {response.status_code}"
|
||||
)
|
||||
|
||||
# Parse JSON response if possible
|
||||
json_response = None
|
||||
if response.headers.get("Content-Type", "").startswith("application/json"):
|
||||
try:
|
||||
json_response = response.json()
|
||||
except json.JSONDecodeError:
|
||||
if expected_status == 200:
|
||||
raise ValueError("Response was not valid JSON")
|
||||
|
||||
return response, json_response
|
||||
|
||||
except (requests.ConnectionError, requests.Timeout) as e:
|
||||
last_exception = e
|
||||
if attempt < retries:
|
||||
# Wait before retrying
|
||||
time.sleep(1)
|
||||
continue
|
||||
break
|
||||
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
|
||||
raise RuntimeError("Failed to make API request")
|
||||
|
||||
return _request
|
||||
1
tests-api/mocks/__init__.py
Normal file
1
tests-api/mocks/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Make tests-api/mocks directory a proper package
|
||||
26
tests-api/mocks/custom_node_manager.py
Normal file
26
tests-api/mocks/custom_node_manager.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Mock CustomNodeManager for testing purposes
|
||||
"""
|
||||
|
||||
class CustomNodeManager:
|
||||
"""
|
||||
Mock implementation of the CustomNodeManager class
|
||||
"""
|
||||
instance = None
|
||||
|
||||
def __init__(self):
|
||||
self.custom_nodes = {}
|
||||
self.node_paths = []
|
||||
self.refresh_timeout = None
|
||||
|
||||
def get_node_path(self, node_class):
|
||||
"""
|
||||
Mock implementation to get the path for a node class
|
||||
"""
|
||||
return self.custom_nodes.get(node_class, None)
|
||||
|
||||
def update_node_paths(self):
|
||||
"""
|
||||
Mock implementation to update node paths
|
||||
"""
|
||||
pass
|
||||
116
tests-api/mocks/patch.py
Normal file
116
tests-api/mocks/patch.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Patch module to mock imports for testing
|
||||
"""
|
||||
import sys
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Import mock modules
|
||||
from mocks.prompt_server import PromptServer
|
||||
from mocks.custom_node_manager import CustomNodeManager
|
||||
|
||||
# Current directory
|
||||
current_dir = Path(__file__).parent.parent # tests-api directory
|
||||
|
||||
# Define mocks
|
||||
class MockModule:
|
||||
"""Base class for mock modules"""
|
||||
pass
|
||||
|
||||
# Create server mock module with PromptServer
|
||||
server_mock = MockModule()
|
||||
server_mock.PromptServer = PromptServer
|
||||
prompt_server_instance = PromptServer()
|
||||
server_mock.PromptServer.instance = prompt_server_instance
|
||||
server_mock.PromptServer.inst = prompt_server_instance
|
||||
|
||||
# Create app mock module with custom_node_manager submodule
|
||||
app_mock = MockModule()
|
||||
app_custom_node_manager = MockModule()
|
||||
app_custom_node_manager.CustomNodeManager = CustomNodeManager
|
||||
app_custom_node_manager.CustomNodeManager.instance = CustomNodeManager()
|
||||
|
||||
# Create utils mock module with json_util submodule
|
||||
utils_mock = MockModule()
|
||||
utils_json_util = MockModule()
|
||||
|
||||
# Create utils.validation and utils.schema_utils submodules
|
||||
utils_validation = MockModule()
|
||||
utils_schema_utils = MockModule()
|
||||
|
||||
# Import actual modules (make sure path is set up correctly)
|
||||
sys.path.insert(0, str(current_dir))
|
||||
|
||||
try:
|
||||
# Import the validation module
|
||||
from utils.validation import load_openapi_spec
|
||||
utils_validation.load_openapi_spec = load_openapi_spec
|
||||
|
||||
# Import all schema_utils functions
|
||||
from utils.schema_utils import (
|
||||
get_all_paths,
|
||||
get_grouped_paths,
|
||||
get_methods_for_path,
|
||||
find_paths_with_security,
|
||||
get_content_types_for_response,
|
||||
get_required_parameters
|
||||
)
|
||||
|
||||
utils_schema_utils.get_all_paths = get_all_paths
|
||||
utils_schema_utils.get_grouped_paths = get_grouped_paths
|
||||
utils_schema_utils.get_methods_for_path = get_methods_for_path
|
||||
utils_schema_utils.find_paths_with_security = find_paths_with_security
|
||||
utils_schema_utils.get_content_types_for_response = get_content_types_for_response
|
||||
utils_schema_utils.get_required_parameters = get_required_parameters
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Error importing test utilities: {e}")
|
||||
# Define dummy functions if imports fail
|
||||
def dummy_load_openapi_spec():
|
||||
"""Dummy function for testing"""
|
||||
return {"paths": {}}
|
||||
utils_validation.load_openapi_spec = dummy_load_openapi_spec
|
||||
|
||||
def dummy_get_all_paths(spec):
|
||||
return list(spec.get("paths", {}).keys())
|
||||
utils_schema_utils.get_all_paths = dummy_get_all_paths
|
||||
|
||||
def dummy_get_grouped_paths(spec):
|
||||
return {}
|
||||
utils_schema_utils.get_grouped_paths = dummy_get_grouped_paths
|
||||
|
||||
def dummy_get_methods_for_path(spec, path):
|
||||
return []
|
||||
utils_schema_utils.get_methods_for_path = dummy_get_methods_for_path
|
||||
|
||||
def dummy_find_paths_with_security(spec, security_scheme=None):
|
||||
return []
|
||||
utils_schema_utils.find_paths_with_security = dummy_find_paths_with_security
|
||||
|
||||
def dummy_get_content_types_for_response(spec, path, method, status_code="200"):
|
||||
return []
|
||||
utils_schema_utils.get_content_types_for_response = dummy_get_content_types_for_response
|
||||
|
||||
def dummy_get_required_parameters(spec, path, method):
|
||||
return []
|
||||
utils_schema_utils.get_required_parameters = dummy_get_required_parameters
|
||||
|
||||
# Add merge_json_recursive from our mock utils
|
||||
from mocks.utils import merge_json_recursive
|
||||
utils_json_util.merge_json_recursive = merge_json_recursive
|
||||
|
||||
# Apply the mocks to sys.modules
|
||||
def apply_mocks():
|
||||
"""Apply all mocks to sys.modules"""
|
||||
sys.modules['server'] = server_mock
|
||||
sys.modules['app'] = app_mock
|
||||
sys.modules['app.custom_node_manager'] = app_custom_node_manager
|
||||
sys.modules['utils'] = utils_mock
|
||||
sys.modules['utils.json_util'] = utils_json_util
|
||||
sys.modules['utils.validation'] = utils_validation
|
||||
sys.modules['utils.schema_utils'] = utils_schema_utils
|
||||
|
||||
# Make sure our actual utils module is importable
|
||||
if current_dir not in sys.path:
|
||||
sys.path.insert(0, str(current_dir))
|
||||
71
tests-api/mocks/prompt_server.py
Normal file
71
tests-api/mocks/prompt_server.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Mock PromptServer for testing purposes
|
||||
"""
|
||||
|
||||
class MockRoutes:
|
||||
"""
|
||||
Mock routing class with method decorators
|
||||
"""
|
||||
def __init__(self):
|
||||
self.routes = {}
|
||||
|
||||
def get(self, path):
|
||||
"""Decorator for GET routes"""
|
||||
def decorator(f):
|
||||
self.routes[('GET', path)] = f
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def post(self, path):
|
||||
"""Decorator for POST routes"""
|
||||
def decorator(f):
|
||||
self.routes[('POST', path)] = f
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def put(self, path):
|
||||
"""Decorator for PUT routes"""
|
||||
def decorator(f):
|
||||
self.routes[('PUT', path)] = f
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def delete(self, path):
|
||||
"""Decorator for DELETE routes"""
|
||||
def decorator(f):
|
||||
self.routes[('DELETE', path)] = f
|
||||
return f
|
||||
return decorator
|
||||
|
||||
|
||||
class PromptServer:
|
||||
"""
|
||||
Mock implementation of the PromptServer class
|
||||
"""
|
||||
instance = None
|
||||
inst = None
|
||||
|
||||
def __init__(self):
|
||||
self.routes = MockRoutes()
|
||||
self.registered_paths = set()
|
||||
self.base_url = "http://127.0.0.1:8188" # Assuming server is running on default port
|
||||
self.queue_lock = None
|
||||
|
||||
def add_route(self, method, path, handler, *args, **kwargs):
|
||||
"""
|
||||
Add a mock route to the server
|
||||
"""
|
||||
self.routes.routes[(method.upper(), path)] = handler
|
||||
self.registered_paths.add(path)
|
||||
|
||||
async def send_msg(self, message, data=None):
|
||||
"""
|
||||
Mock send_msg method (does nothing in the mock)
|
||||
"""
|
||||
pass
|
||||
|
||||
def send_sync(self, message, data=None):
|
||||
"""
|
||||
Mock send_sync method (does nothing in the mock)
|
||||
"""
|
||||
pass
|
||||
20
tests-api/mocks/utils.py
Normal file
20
tests-api/mocks/utils.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Mock utils module for testing purposes
|
||||
"""
|
||||
|
||||
def merge_json_recursive(a, b):
|
||||
"""
|
||||
Mock implementation of merge_json_recursive
|
||||
"""
|
||||
if isinstance(a, dict) and isinstance(b, dict):
|
||||
result = a.copy()
|
||||
for key, value in b.items():
|
||||
if key in result and isinstance(result[key], (dict, list)) and isinstance(value, (dict, list)):
|
||||
result[key] = merge_json_recursive(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
elif isinstance(a, list) and isinstance(b, list):
|
||||
return a + b
|
||||
else:
|
||||
return b
|
||||
382
tests-api/openapi.yaml
Normal file
382
tests-api/openapi.yaml
Normal file
@@ -0,0 +1,382 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: ComfyUI-Manager API
|
||||
description: API for managing ComfyUI extensions, custom nodes, and models
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: ComfyUI Community
|
||||
url: https://github.com/comfyanonymous/ComfyUI
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8188
|
||||
description: Local ComfyUI server
|
||||
|
||||
paths:
|
||||
/customnode/getlist:
|
||||
get:
|
||||
summary: Get the list of custom nodes
|
||||
description: Returns the list of custom nodes from all configured channels
|
||||
parameters:
|
||||
- name: mode
|
||||
in: query
|
||||
description: "The mode to retrieve (local=installed nodes, remote=available nodes)"
|
||||
schema:
|
||||
type: string
|
||||
enum: [local, remote]
|
||||
default: remote
|
||||
responses:
|
||||
'200':
|
||||
description: List of custom nodes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
nodes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CustomNode'
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
/customnode/get_node_mappings:
|
||||
get:
|
||||
summary: Get mappings between node class names and their custom nodes
|
||||
description: Returns mappings that help identify which custom node package provides specific node classes
|
||||
parameters:
|
||||
- name: mode
|
||||
in: query
|
||||
description: "The mode for mappings (local=installed nodes, nickname=node nicknames)"
|
||||
schema:
|
||||
type: string
|
||||
enum: [local, nickname]
|
||||
default: local
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Node mappings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
/customnode/get_node_alternatives:
|
||||
get:
|
||||
summary: Get alternative nodes for specific node classes
|
||||
description: Returns alternative implementations of node classes from different custom node packages
|
||||
parameters:
|
||||
- name: mode
|
||||
in: query
|
||||
description: "The mode to retrieve alternatives (local=installed nodes, remote=all available nodes)"
|
||||
schema:
|
||||
type: string
|
||||
enum: [local, remote]
|
||||
default: remote
|
||||
responses:
|
||||
'200':
|
||||
description: Node alternatives
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
/externalmodel/getlist:
|
||||
get:
|
||||
summary: Get the list of external models
|
||||
description: Returns the list of models from all configured channels
|
||||
parameters:
|
||||
- name: mode
|
||||
in: query
|
||||
description: "The mode to retrieve (local=installed models, remote=available models)"
|
||||
schema:
|
||||
type: string
|
||||
enum: [local, remote]
|
||||
default: remote
|
||||
responses:
|
||||
'200':
|
||||
description: List of external models
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
models:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ExternalModel'
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
/manager/get_config:
|
||||
get:
|
||||
summary: Get manager configuration
|
||||
description: Returns the current configuration of ComfyUI-Manager
|
||||
parameters:
|
||||
- name: key
|
||||
in: query
|
||||
description: "The configuration key to retrieve"
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Configuration value
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
'400':
|
||||
description: Invalid key or missing parameter
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
/manager/set_config:
|
||||
post:
|
||||
summary: Set manager configuration
|
||||
description: Updates the configuration of ComfyUI-Manager
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: "The configuration key to update"
|
||||
value:
|
||||
type: string
|
||||
description: "The new value for the configuration key"
|
||||
responses:
|
||||
'200':
|
||||
description: Configuration updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
'400':
|
||||
description: Invalid key or value
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
/snapshot/getlist:
|
||||
get:
|
||||
summary: Get the list of snapshots
|
||||
description: Returns the list of saved snapshots
|
||||
responses:
|
||||
'200':
|
||||
description: List of snapshots
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
snapshots:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Snapshot'
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
/comfyui_manager/queue/status:
|
||||
get:
|
||||
summary: Get queue status
|
||||
description: Returns the current status of the operation queue
|
||||
responses:
|
||||
'200':
|
||||
description: Queue status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QueueStatus'
|
||||
'500':
|
||||
description: Server error
|
||||
|
||||
components:
|
||||
schemas:
|
||||
CustomNode:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- title
|
||||
- reference
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "Internal name/ID of the custom node"
|
||||
title:
|
||||
type: string
|
||||
description: "Display title of the custom node"
|
||||
reference:
|
||||
type: string
|
||||
description: "Reference URL (usually GitHub repository URL)"
|
||||
description:
|
||||
type: string
|
||||
description: "Description of what the custom node does"
|
||||
install_type:
|
||||
type: string
|
||||
enum: [git, pip, copy]
|
||||
description: "Installation method for the custom node"
|
||||
files:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "List of files provided by this custom node"
|
||||
node_class_names:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "List of node class names provided by this custom node"
|
||||
installed:
|
||||
type: boolean
|
||||
description: "Whether the custom node is installed"
|
||||
version:
|
||||
type: string
|
||||
description: "Version of the custom node"
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "Tags associated with the custom node"
|
||||
|
||||
ExternalModel:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
- url
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "Name of the model"
|
||||
type:
|
||||
type: string
|
||||
description: "Type of the model (checkpoint, lora, embedding, etc.)"
|
||||
url:
|
||||
type: string
|
||||
description: "Download URL for the model"
|
||||
description:
|
||||
type: string
|
||||
description: "Description of the model"
|
||||
size:
|
||||
type: integer
|
||||
description: "Size of the model in bytes"
|
||||
installed:
|
||||
type: boolean
|
||||
description: "Whether the model is installed"
|
||||
version:
|
||||
type: string
|
||||
description: "Version of the model"
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "Tags associated with the model"
|
||||
|
||||
Snapshot:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- date
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "Name of the snapshot"
|
||||
date:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Date when the snapshot was created"
|
||||
description:
|
||||
type: string
|
||||
description: "Description of the snapshot"
|
||||
nodes:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "List of custom nodes in the snapshot"
|
||||
models:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "List of models in the snapshot"
|
||||
|
||||
QueueStatus:
|
||||
type: object
|
||||
properties:
|
||||
pending:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/QueueItem'
|
||||
description: "List of pending operations in the queue"
|
||||
completed:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/QueueItem'
|
||||
description: "List of completed operations in the queue"
|
||||
failed:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/QueueItem'
|
||||
description: "List of failed operations in the queue"
|
||||
running:
|
||||
type: boolean
|
||||
description: "Whether the queue is currently running"
|
||||
|
||||
QueueItem:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
- target
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: "Unique ID of the queue item"
|
||||
type:
|
||||
type: string
|
||||
enum: [install, update, uninstall]
|
||||
description: "Type of operation"
|
||||
target:
|
||||
type: string
|
||||
description: "Target of the operation (e.g., custom node name, model name)"
|
||||
status:
|
||||
type: string
|
||||
enum: [pending, processing, completed, failed]
|
||||
description: "Current status of the operation"
|
||||
error:
|
||||
type: string
|
||||
description: "Error message if the operation failed"
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Time when the operation was added to the queue"
|
||||
completed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Time when the operation was completed"
|
||||
|
||||
securitySchemes:
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
description: "API key for authentication"
|
||||
6
tests-api/requirements-test.txt
Normal file
6
tests-api/requirements-test.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
pytest>=7.3.1
|
||||
requests>=2.31.0
|
||||
openapi-spec-validator>=0.6.0
|
||||
jsonschema>=4.17.3
|
||||
pytest-asyncio>=0.21.0
|
||||
pyyaml>=6.0
|
||||
270
tests-api/test_config_api.py
Normal file
270
tests-api/test_config_api.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Tests for configuration endpoints.
|
||||
"""
|
||||
import pytest
|
||||
from typing import Callable, Dict, List, Tuple
|
||||
|
||||
from utils.validation import validate_response
|
||||
|
||||
|
||||
def test_get_preview_method(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test getting the current preview method.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/manager/preview_method"
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify the response is one of the valid preview methods
|
||||
assert response.text in ["auto", "latent2rgb", "taesd", "none"]
|
||||
|
||||
|
||||
def test_get_db_mode(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test getting the current database mode.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/manager/db_mode"
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify the response is one of the valid database modes
|
||||
assert response.text in ["channel", "local", "remote"]
|
||||
|
||||
|
||||
def test_get_component_policy(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test getting the current component policy.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/manager/policy/component"
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Component policy could be any string
|
||||
assert response.text is not None
|
||||
|
||||
|
||||
def test_get_update_policy(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test getting the current update policy.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/manager/policy/update"
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify the response is one of the valid update policies
|
||||
assert response.text in ["stable", "nightly", "nightly-comfyui"]
|
||||
|
||||
|
||||
def test_get_channel_url_list(
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict
|
||||
):
|
||||
"""
|
||||
Test getting the channel URL list.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/manager/channel_url_list"
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate response structure against the schema
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path=path,
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
# Verify the response contains the expected fields
|
||||
assert "selected" in json_data
|
||||
assert "list" in json_data
|
||||
assert isinstance(json_data["list"], list)
|
||||
|
||||
# Each channel should have a name and URL
|
||||
if json_data["list"]:
|
||||
first_channel = json_data["list"][0]
|
||||
assert "name" in first_channel
|
||||
assert "url" in first_channel
|
||||
|
||||
|
||||
def test_get_manager_version(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test getting the manager version.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/manager/version"
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify the response is a version string
|
||||
assert response.text.startswith("V") # Version strings start with V
|
||||
|
||||
|
||||
def test_get_manager_notice(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test getting the manager notice.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/manager/notice"
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify the response is HTML content
|
||||
assert response.headers.get("Content-Type", "").startswith("text/html") or "ComfyUI" in response.text
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying operations")
|
||||
class TestConfigChanges:
|
||||
"""
|
||||
Tests for changing configuration settings.
|
||||
These are skipped to avoid modifying state in automated tests.
|
||||
"""
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def save_original_config(self, api_request: Callable):
|
||||
"""
|
||||
Save the original configuration to restore after tests.
|
||||
"""
|
||||
# Save original values
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path="/manager/preview_method",
|
||||
expected_status=200,
|
||||
)
|
||||
self.original_preview_method = response.text
|
||||
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path="/manager/db_mode",
|
||||
expected_status=200,
|
||||
)
|
||||
self.original_db_mode = response.text
|
||||
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path="/manager/policy/update",
|
||||
expected_status=200,
|
||||
)
|
||||
self.original_update_policy = response.text
|
||||
|
||||
yield
|
||||
|
||||
# Restore original values
|
||||
api_request(
|
||||
method="get",
|
||||
path="/manager/preview_method",
|
||||
params={"value": self.original_preview_method},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
api_request(
|
||||
method="get",
|
||||
path="/manager/db_mode",
|
||||
params={"value": self.original_db_mode},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
api_request(
|
||||
method="get",
|
||||
path="/manager/policy/update",
|
||||
params={"value": self.original_update_policy},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
def test_set_preview_method(self, api_request: Callable):
|
||||
"""
|
||||
Test setting the preview method.
|
||||
"""
|
||||
# Set to a different value (taesd)
|
||||
api_request(
|
||||
method="get",
|
||||
path="/manager/preview_method",
|
||||
params={"value": "taesd"},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify it was changed
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path="/manager/preview_method",
|
||||
expected_status=200,
|
||||
)
|
||||
assert response.text == "taesd"
|
||||
|
||||
def test_set_db_mode(self, api_request: Callable):
|
||||
"""
|
||||
Test setting the database mode.
|
||||
"""
|
||||
# Set to local mode
|
||||
api_request(
|
||||
method="get",
|
||||
path="/manager/db_mode",
|
||||
params={"value": "local"},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify it was changed
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path="/manager/db_mode",
|
||||
expected_status=200,
|
||||
)
|
||||
assert response.text == "local"
|
||||
|
||||
def test_set_update_policy(self, api_request: Callable):
|
||||
"""
|
||||
Test setting the update policy.
|
||||
"""
|
||||
# Set to stable
|
||||
api_request(
|
||||
method="get",
|
||||
path="/manager/policy/update",
|
||||
params={"value": "stable"},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify it was changed
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path="/manager/policy/update",
|
||||
expected_status=200,
|
||||
)
|
||||
assert response.text == "stable"
|
||||
200
tests-api/test_customnode_api.py
Normal file
200
tests-api/test_customnode_api.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Tests for custom node management endpoints.
|
||||
"""
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, Tuple
|
||||
|
||||
from utils.validation import validate_response
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
["local", "remote"]
|
||||
)
|
||||
def test_get_custom_node_list(
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict,
|
||||
mode: str
|
||||
):
|
||||
"""
|
||||
Test the endpoint for listing custom nodes.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/customnode/getlist"
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
params={"mode": mode, "skip_update": "true"},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate response structure against the schema
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path=path,
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
# Verify the response contains the expected fields
|
||||
assert "channel" in json_data
|
||||
assert "node_packs" in json_data
|
||||
assert isinstance(json_data["node_packs"], dict)
|
||||
|
||||
# If there are any node packs, verify they have the expected structure
|
||||
if json_data["node_packs"]:
|
||||
# Take the first node pack to validate
|
||||
first_node_pack = next(iter(json_data["node_packs"].values()))
|
||||
assert "title" in first_node_pack
|
||||
assert "name" in first_node_pack
|
||||
|
||||
|
||||
def test_get_installed_nodes(
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict
|
||||
):
|
||||
"""
|
||||
Test the endpoint for listing installed nodes.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/customnode/installed"
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate response structure against the schema
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path=path,
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
# Verify the response is a dictionary of node packs
|
||||
assert isinstance(json_data, dict)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
["local", "nickname"]
|
||||
)
|
||||
def test_get_node_mappings(
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict,
|
||||
mode: str
|
||||
):
|
||||
"""
|
||||
Test the endpoint for getting node-to-package mappings.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/customnode/getmappings"
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
params={"mode": mode},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate response structure against the schema
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path=path,
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
# Verify the response is a dictionary mapping extension IDs to node info
|
||||
assert isinstance(json_data, dict)
|
||||
|
||||
# If there are any mappings, verify they have the expected structure
|
||||
if json_data:
|
||||
# Take the first mapping to validate
|
||||
first_mapping = next(iter(json_data.values()))
|
||||
assert isinstance(first_mapping, list)
|
||||
assert len(first_mapping) == 2
|
||||
assert isinstance(first_mapping[0], list) # List of node classes
|
||||
assert isinstance(first_mapping[1], dict) # Metadata
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
["local", "remote"]
|
||||
)
|
||||
def test_get_node_alternatives(
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict,
|
||||
mode: str
|
||||
):
|
||||
"""
|
||||
Test the endpoint for getting alternative node options.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/customnode/alternatives"
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
params={"mode": mode},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate response structure against the schema
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path=path,
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
# Verify the response is a dictionary
|
||||
assert isinstance(json_data, dict)
|
||||
|
||||
|
||||
def test_fetch_updates(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test the endpoint for fetching updates.
|
||||
This might modify state, so we just check for a valid response.
|
||||
"""
|
||||
# Make the API request with skip_update=true to avoid actual updates
|
||||
path = "/customnode/fetch_updates"
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
params={"mode": "local"},
|
||||
# Don't validate JSON since this endpoint doesn't return JSON
|
||||
expected_status=200,
|
||||
retry_on_error=False, # Don't retry as this might have side effects
|
||||
)
|
||||
|
||||
# Just check the status code is as expected (covered by api_request)
|
||||
assert response.status_code in [200, 201]
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Queue endpoints are better tested with queue operations")
|
||||
def test_queue_update_all(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test the endpoint for queuing updates for all nodes.
|
||||
Skipping as this would actually modify the installation.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Security-restricted endpoint")
|
||||
def test_install_node_via_git_url(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test the endpoint for installing a node via Git URL.
|
||||
Skipping as this requires high security level and would modify the installation.
|
||||
"""
|
||||
pass
|
||||
23
tests-api/test_import.py
Normal file
23
tests-api/test_import.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Print current working directory
|
||||
print(f"Current directory: {os.getcwd()}")
|
||||
|
||||
# Print module search path
|
||||
print(f"System path: {sys.path}")
|
||||
|
||||
# Try to import
|
||||
try:
|
||||
from utils.validation import load_openapi_spec
|
||||
print("Import successful!")
|
||||
except ImportError as e:
|
||||
print(f"Import error: {e}")
|
||||
|
||||
# Try direct import
|
||||
try:
|
||||
sys.path.insert(0, os.path.join(os.getcwd(), "custom_nodes/ComfyUI-Manager/tests-api"))
|
||||
from utils.validation import load_openapi_spec
|
||||
print("Direct import successful!")
|
||||
except ImportError as e:
|
||||
print(f"Direct import error: {e}")
|
||||
62
tests-api/test_model_api.py
Normal file
62
tests-api/test_model_api.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Tests for model management endpoints.
|
||||
These features are scheduled for deprecation, so tests are minimal.
|
||||
"""
|
||||
import pytest
|
||||
from typing import Callable, Dict
|
||||
|
||||
from utils.validation import validate_response
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
["local", "remote"]
|
||||
)
|
||||
def test_get_external_model_list(
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict,
|
||||
mode: str
|
||||
):
|
||||
"""
|
||||
Test the endpoint for listing external models.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/externalmodel/getlist"
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
params={"mode": mode},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate response structure against the schema
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path=path,
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
# Verify the response contains the expected fields
|
||||
assert "models" in json_data
|
||||
assert isinstance(json_data["models"], list)
|
||||
|
||||
# If there are any models, verify they have the expected structure
|
||||
if json_data["models"]:
|
||||
first_model = json_data["models"][0]
|
||||
assert "name" in first_model
|
||||
assert "type" in first_model
|
||||
assert "url" in first_model
|
||||
assert "filename" in first_model
|
||||
assert "installed" in first_model
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying operation that requires auth")
|
||||
def test_install_model():
|
||||
"""
|
||||
Test queuing a model installation.
|
||||
Skipped to avoid modifying state and requires authentication.
|
||||
This feature is also scheduled for deprecation.
|
||||
"""
|
||||
pass
|
||||
213
tests-api/test_queue_api.py
Normal file
213
tests-api/test_queue_api.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
Tests for queue management endpoints.
|
||||
"""
|
||||
import pytest
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, Tuple
|
||||
|
||||
from utils.validation import validate_response
|
||||
|
||||
|
||||
def test_get_queue_status(
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict
|
||||
):
|
||||
"""
|
||||
Test the endpoint for getting queue status.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/manager/queue/status"
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate response structure against the schema
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path=path,
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
# Verify the response contains the expected fields
|
||||
assert "total_count" in json_data
|
||||
assert "done_count" in json_data
|
||||
assert "in_progress_count" in json_data
|
||||
assert "is_processing" in json_data
|
||||
|
||||
# Type checks
|
||||
assert isinstance(json_data["total_count"], int)
|
||||
assert isinstance(json_data["done_count"], int)
|
||||
assert isinstance(json_data["in_progress_count"], int)
|
||||
assert isinstance(json_data["is_processing"], bool)
|
||||
|
||||
|
||||
def test_reset_queue(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test the endpoint for resetting the queue.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/manager/queue/reset"
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Now check the queue status to verify it was reset
|
||||
response2, json_data = api_request(
|
||||
method="get",
|
||||
path="/manager/queue/status",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Queue should be empty after reset
|
||||
assert json_data["total_count"] == json_data["done_count"] + json_data["in_progress_count"]
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying operation that requires auth")
|
||||
def test_queue_install_node():
|
||||
"""
|
||||
Test queuing a node installation.
|
||||
Skipped to avoid modifying state and requires authentication.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying operation that requires auth")
|
||||
def test_queue_update_node():
|
||||
"""
|
||||
Test queuing a node update.
|
||||
Skipped to avoid modifying state and requires authentication.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying operation that requires auth")
|
||||
def test_queue_uninstall_node():
|
||||
"""
|
||||
Test queuing a node uninstallation.
|
||||
Skipped to avoid modifying state and requires authentication.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying operation")
|
||||
def test_queue_start():
|
||||
"""
|
||||
Test starting the queue.
|
||||
Skipped to avoid modifying state.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TestQueueOperations:
|
||||
"""
|
||||
Test a complete queue workflow.
|
||||
These tests are grouped to ensure proper sequencing but are still skipped
|
||||
to avoid modifying state in automated tests.
|
||||
"""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def node_data(self) -> Dict:
|
||||
"""
|
||||
Create test data for a node operation.
|
||||
"""
|
||||
# This would be replaced with actual data for a known safe node
|
||||
return {
|
||||
"ui_id": "test_node_1",
|
||||
"id": "comfyui-manager", # Manager itself
|
||||
"version": "latest",
|
||||
"channel": "default",
|
||||
"mode": "local",
|
||||
}
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying operation")
|
||||
def test_queue_operation_sequence(
|
||||
self,
|
||||
api_request: Callable,
|
||||
node_data: Dict
|
||||
):
|
||||
"""
|
||||
Test the queue operation sequence.
|
||||
"""
|
||||
# 1. Reset the queue
|
||||
api_request(
|
||||
method="get",
|
||||
path="/manager/queue/reset",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# 2. Queue a node operation (we'll use the manager itself)
|
||||
api_request(
|
||||
method="post",
|
||||
path="/manager/queue/update",
|
||||
json_data=node_data,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# 3. Check queue status - should have one operation
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path="/manager/queue/status",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
assert json_data["total_count"] > 0
|
||||
assert not json_data["is_processing"] # Queue hasn't started yet
|
||||
|
||||
# 4. Start the queue
|
||||
api_request(
|
||||
method="get",
|
||||
path="/manager/queue/start",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# 5. Check queue status again - should be processing
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path="/manager/queue/status",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Queue should be processing or already done
|
||||
assert json_data["is_processing"] or json_data["done_count"] == json_data["total_count"]
|
||||
|
||||
# 6. Wait for queue to complete (with timeout)
|
||||
max_wait_time = 60 # seconds
|
||||
start_time = time.time()
|
||||
completed = False
|
||||
|
||||
while time.time() - start_time < max_wait_time:
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path="/manager/queue/status",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
if json_data["done_count"] == json_data["total_count"] and not json_data["is_processing"]:
|
||||
completed = True
|
||||
break
|
||||
|
||||
time.sleep(2) # Wait before checking again
|
||||
|
||||
assert completed, "Queue did not complete within timeout period"
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying operation")
|
||||
def test_concurrent_queue_operations(
|
||||
self,
|
||||
api_request: Callable,
|
||||
node_data: Dict
|
||||
):
|
||||
"""
|
||||
Test concurrent queue operations.
|
||||
"""
|
||||
# This would test adding multiple operations to the queue
|
||||
# and verifying they all complete correctly
|
||||
pass
|
||||
198
tests-api/test_snapshot_api.py
Normal file
198
tests-api/test_snapshot_api.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
Tests for snapshot management endpoints.
|
||||
"""
|
||||
import pytest
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
from utils.validation import validate_response
|
||||
|
||||
|
||||
def test_get_snapshot_list(
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict
|
||||
):
|
||||
"""
|
||||
Test the endpoint for listing snapshots.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/snapshot/getlist"
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate response structure against the schema
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path=path,
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
# Verify the response contains the expected fields
|
||||
assert "items" in json_data
|
||||
assert isinstance(json_data["items"], list)
|
||||
|
||||
|
||||
def test_get_current_snapshot(
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict
|
||||
):
|
||||
"""
|
||||
Test the endpoint for getting the current snapshot.
|
||||
"""
|
||||
# Make the API request
|
||||
path = "/snapshot/get_current"
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path=path,
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate response structure against the schema
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path=path,
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
# Check for basic snapshot structure
|
||||
assert "snapshot_date" in json_data
|
||||
assert "custom_nodes" in json_data
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="This test creates a snapshot which is a state-modifying operation")
|
||||
def test_save_snapshot(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test the endpoint for saving a new snapshot.
|
||||
Skipped to avoid modifying state in tests.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="This test removes a snapshot which is a destructive operation")
|
||||
def test_remove_snapshot(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test the endpoint for removing a snapshot.
|
||||
Skipped to avoid modifying state in tests.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="This test restores a snapshot which is a state-modifying operation")
|
||||
def test_restore_snapshot(
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test the endpoint for restoring a snapshot.
|
||||
Skipped to avoid modifying state in tests.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TestSnapshotWorkflow:
|
||||
"""
|
||||
Test the complete snapshot workflow (create, list, get, remove).
|
||||
These tests are grouped to ensure proper sequencing but are still skipped
|
||||
to avoid modifying state in automated tests.
|
||||
"""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def snapshot_name(self) -> str:
|
||||
"""
|
||||
Generate a unique snapshot name for testing.
|
||||
"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return f"test_snapshot_{timestamp}"
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying test")
|
||||
def test_create_snapshot(
|
||||
self,
|
||||
api_request: Callable,
|
||||
snapshot_name: str
|
||||
):
|
||||
"""
|
||||
Test creating a snapshot.
|
||||
"""
|
||||
# Make the API request to save a snapshot
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path="/snapshot/save",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify a snapshot was created (would need to check the snapshot list)
|
||||
response2, json_data = api_request(
|
||||
method="get",
|
||||
path="/snapshot/getlist",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# The most recently created snapshot should be first in the list
|
||||
assert json_data["items"]
|
||||
|
||||
# Store the snapshot name for later tests
|
||||
self.actual_snapshot_name = json_data["items"][0]
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying test")
|
||||
def test_get_snapshot_details(
|
||||
self,
|
||||
api_request: Callable,
|
||||
openapi_spec: Dict
|
||||
):
|
||||
"""
|
||||
Test getting details of the created snapshot.
|
||||
"""
|
||||
# This would check the current snapshot, not a specific one
|
||||
# since there's no direct API to get a specific snapshot
|
||||
response, json_data = api_request(
|
||||
method="get",
|
||||
path="/snapshot/get_current",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Validate the snapshot data
|
||||
assert json_data is not None
|
||||
validate_response(
|
||||
response_data=json_data,
|
||||
path="/snapshot/get_current",
|
||||
method="get",
|
||||
spec=openapi_spec,
|
||||
)
|
||||
|
||||
@pytest.mark.skip(reason="State-modifying test")
|
||||
def test_remove_test_snapshot(
|
||||
self,
|
||||
api_request: Callable
|
||||
):
|
||||
"""
|
||||
Test removing the test snapshot.
|
||||
"""
|
||||
# Make the API request to remove the snapshot
|
||||
response, _ = api_request(
|
||||
method="get",
|
||||
path="/snapshot/remove",
|
||||
params={"target": self.actual_snapshot_name},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# Verify the snapshot was removed
|
||||
response2, json_data = api_request(
|
||||
method="get",
|
||||
path="/snapshot/getlist",
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
# The snapshot should no longer be in the list
|
||||
assert self.actual_snapshot_name not in json_data["items"]
|
||||
150
tests-api/test_spec_validation.py
Normal file
150
tests-api/test_spec_validation.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Tests for validating the OpenAPI specification.
|
||||
"""
|
||||
import json
|
||||
import pytest
|
||||
import yaml
|
||||
from typing import Dict, Any, List, Tuple
|
||||
from pathlib import Path
|
||||
from openapi_spec_validator import validate_spec
|
||||
from utils.validation import load_openapi_spec
|
||||
from utils.schema_utils import (
|
||||
get_all_paths,
|
||||
get_methods_for_path,
|
||||
find_paths_with_security,
|
||||
get_required_parameters
|
||||
)
|
||||
|
||||
|
||||
def test_spec_is_valid():
|
||||
"""
|
||||
Test that the OpenAPI specification is valid according to the spec validator.
|
||||
"""
|
||||
spec = load_openapi_spec()
|
||||
validate_spec(spec)
|
||||
|
||||
|
||||
def test_spec_has_info():
|
||||
"""
|
||||
Test that the OpenAPI specification has basic info.
|
||||
"""
|
||||
spec = load_openapi_spec()
|
||||
|
||||
assert "info" in spec
|
||||
assert "title" in spec["info"]
|
||||
assert "version" in spec["info"]
|
||||
assert spec["info"]["title"] == "ComfyUI-Manager API"
|
||||
|
||||
|
||||
def test_spec_has_paths():
|
||||
"""
|
||||
Test that the OpenAPI specification has paths defined.
|
||||
"""
|
||||
spec = load_openapi_spec()
|
||||
|
||||
assert "paths" in spec
|
||||
assert len(spec["paths"]) > 0
|
||||
|
||||
|
||||
def test_paths_have_responses():
|
||||
"""
|
||||
Test that all paths have responses defined.
|
||||
"""
|
||||
spec = load_openapi_spec()
|
||||
|
||||
for path, path_item in spec["paths"].items():
|
||||
for method, operation in path_item.items():
|
||||
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
|
||||
continue
|
||||
|
||||
assert "responses" in operation, f"Path {path} method {method} has no responses"
|
||||
assert len(operation["responses"]) > 0, f"Path {path} method {method} has empty responses"
|
||||
|
||||
|
||||
def test_responses_have_schemas():
|
||||
"""
|
||||
Test that responses with application/json content type have schemas.
|
||||
"""
|
||||
spec = load_openapi_spec()
|
||||
|
||||
for path, path_item in spec["paths"].items():
|
||||
for method, operation in path_item.items():
|
||||
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
|
||||
continue
|
||||
|
||||
for status, response in operation["responses"].items():
|
||||
if "content" not in response:
|
||||
continue
|
||||
|
||||
if "application/json" in response["content"]:
|
||||
assert "schema" in response["content"]["application/json"], (
|
||||
f"Path {path} method {method} status {status} "
|
||||
f"application/json content has no schema"
|
||||
)
|
||||
|
||||
|
||||
def test_required_parameters_have_schemas():
|
||||
"""
|
||||
Test that all required parameters have schemas.
|
||||
"""
|
||||
spec = load_openapi_spec()
|
||||
|
||||
for path, path_item in spec["paths"].items():
|
||||
for method, operation in path_item.items():
|
||||
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
|
||||
continue
|
||||
|
||||
if "parameters" not in operation:
|
||||
continue
|
||||
|
||||
for param in operation["parameters"]:
|
||||
if param.get("required", False):
|
||||
assert "schema" in param, (
|
||||
f"Path {path} method {method} required parameter {param.get('name')} has no schema"
|
||||
)
|
||||
|
||||
|
||||
def test_security_schemes_defined():
|
||||
"""
|
||||
Test that security schemes are properly defined.
|
||||
"""
|
||||
spec = load_openapi_spec()
|
||||
|
||||
# Get paths requiring security
|
||||
secure_paths = find_paths_with_security(spec)
|
||||
|
||||
if secure_paths:
|
||||
assert "components" in spec, "Spec has secure paths but no components"
|
||||
assert "securitySchemes" in spec["components"], "Spec has secure paths but no securitySchemes"
|
||||
|
||||
# Check each security reference is defined
|
||||
for path, method in secure_paths:
|
||||
operation = spec["paths"][path][method]
|
||||
for security_req in operation["security"]:
|
||||
for scheme_name in security_req:
|
||||
assert scheme_name in spec["components"]["securitySchemes"], (
|
||||
f"Security scheme {scheme_name} used by {method.upper()} {path} "
|
||||
f"is not defined in components.securitySchemes"
|
||||
)
|
||||
|
||||
|
||||
def test_common_endpoint_groups_present():
|
||||
"""
|
||||
Test that the spec includes the main endpoint groups.
|
||||
"""
|
||||
spec = load_openapi_spec()
|
||||
paths = get_all_paths(spec)
|
||||
|
||||
# Define the expected endpoint prefixes
|
||||
expected_prefixes = [
|
||||
"/customnode/",
|
||||
"/externalmodel/",
|
||||
"/manager/",
|
||||
"/snapshot/",
|
||||
"/comfyui_manager/",
|
||||
]
|
||||
|
||||
# Check that at least one path exists for each expected prefix
|
||||
for prefix in expected_prefixes:
|
||||
matching_paths = [p for p in paths if p.startswith(prefix)]
|
||||
assert matching_paths, f"No endpoints found with prefix {prefix}"
|
||||
1
tests-api/utils/__init__.py
Normal file
1
tests-api/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Make utils directory a proper package
|
||||
174
tests-api/utils/schema_utils.py
Normal file
174
tests-api/utils/schema_utils.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Schema utilities for extracting and manipulating OpenAPI schemas.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
from .validation import load_openapi_spec
|
||||
|
||||
|
||||
def get_all_paths(spec: Dict[str, Any]) -> List[str]:
|
||||
"""
|
||||
Get all paths defined in the OpenAPI specification.
|
||||
|
||||
Args:
|
||||
spec: The OpenAPI specification
|
||||
|
||||
Returns:
|
||||
List of all paths
|
||||
"""
|
||||
return list(spec.get("paths", {}).keys())
|
||||
|
||||
|
||||
def get_grouped_paths(spec: Dict[str, Any]) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Group paths by their top-level segment.
|
||||
|
||||
Args:
|
||||
spec: The OpenAPI specification
|
||||
|
||||
Returns:
|
||||
Dictionary mapping top-level segments to lists of paths
|
||||
"""
|
||||
result = {}
|
||||
|
||||
for path in get_all_paths(spec):
|
||||
segments = path.strip("/").split("/")
|
||||
if not segments:
|
||||
continue
|
||||
|
||||
top_segment = segments[0]
|
||||
if top_segment not in result:
|
||||
result[top_segment] = []
|
||||
|
||||
result[top_segment].append(path)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_methods_for_path(spec: Dict[str, Any], path: str) -> List[str]:
|
||||
"""
|
||||
Get all HTTP methods defined for a path.
|
||||
|
||||
Args:
|
||||
spec: The OpenAPI specification
|
||||
path: The API path
|
||||
|
||||
Returns:
|
||||
List of HTTP methods (lowercase)
|
||||
"""
|
||||
if path not in spec.get("paths", {}):
|
||||
return []
|
||||
|
||||
return [
|
||||
method.lower()
|
||||
for method in spec["paths"][path].keys()
|
||||
if method.lower() in {"get", "post", "put", "delete", "patch", "options", "head"}
|
||||
]
|
||||
|
||||
|
||||
def find_paths_with_security(
|
||||
spec: Dict[str, Any],
|
||||
security_scheme: Optional[str] = None
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
Find all paths that require security.
|
||||
|
||||
Args:
|
||||
spec: The OpenAPI specification
|
||||
security_scheme: Optional specific security scheme to filter by
|
||||
|
||||
Returns:
|
||||
List of (path, method) tuples that require security
|
||||
"""
|
||||
result = []
|
||||
|
||||
for path, path_item in spec.get("paths", {}).items():
|
||||
for method, operation in path_item.items():
|
||||
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
|
||||
continue
|
||||
|
||||
if "security" in operation:
|
||||
if security_scheme is None:
|
||||
result.append((path, method.lower()))
|
||||
else:
|
||||
# Check if this security scheme is required
|
||||
for security_req in operation["security"]:
|
||||
if security_scheme in security_req:
|
||||
result.append((path, method.lower()))
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_content_types_for_response(
|
||||
spec: Dict[str, Any],
|
||||
path: str,
|
||||
method: str,
|
||||
status_code: str = "200"
|
||||
) -> List[str]:
|
||||
"""
|
||||
Get content types defined for a response.
|
||||
|
||||
Args:
|
||||
spec: The OpenAPI specification
|
||||
path: The API path
|
||||
method: The HTTP method
|
||||
status_code: The HTTP status code
|
||||
|
||||
Returns:
|
||||
List of content types
|
||||
"""
|
||||
method = method.lower()
|
||||
|
||||
if path not in spec["paths"]:
|
||||
return []
|
||||
|
||||
if method not in spec["paths"][path]:
|
||||
return []
|
||||
|
||||
if "responses" not in spec["paths"][path][method]:
|
||||
return []
|
||||
|
||||
if status_code not in spec["paths"][path][method]["responses"]:
|
||||
return []
|
||||
|
||||
response_def = spec["paths"][path][method]["responses"][status_code]
|
||||
|
||||
if "content" not in response_def:
|
||||
return []
|
||||
|
||||
return list(response_def["content"].keys())
|
||||
|
||||
|
||||
def get_required_parameters(
|
||||
spec: Dict[str, Any],
|
||||
path: str,
|
||||
method: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all required parameters for a path/method.
|
||||
|
||||
Args:
|
||||
spec: The OpenAPI specification
|
||||
path: The API path
|
||||
method: The HTTP method
|
||||
|
||||
Returns:
|
||||
List of parameter objects that are required
|
||||
"""
|
||||
method = method.lower()
|
||||
|
||||
if path not in spec["paths"]:
|
||||
return []
|
||||
|
||||
if method not in spec["paths"][path]:
|
||||
return []
|
||||
|
||||
if "parameters" not in spec["paths"][path][method]:
|
||||
return []
|
||||
|
||||
return [
|
||||
param for param in spec["paths"][path][method]["parameters"]
|
||||
if param.get("required", False)
|
||||
]
|
||||
155
tests-api/utils/validation.py
Normal file
155
tests-api/utils/validation.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
Validation utilities for API tests.
|
||||
"""
|
||||
import json
|
||||
import jsonschema
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
|
||||
def load_openapi_spec(spec_path: Union[str, Path] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Load the OpenAPI specification document.
|
||||
|
||||
Args:
|
||||
spec_path: Path to the OpenAPI specification file
|
||||
|
||||
Returns:
|
||||
The OpenAPI specification as a dictionary
|
||||
"""
|
||||
if spec_path is None:
|
||||
# Default to the root openapi.yaml file
|
||||
spec_path = Path(__file__).parents[2] / "openapi.yaml"
|
||||
|
||||
with open(spec_path, "r") as f:
|
||||
if str(spec_path).endswith(".yaml") or str(spec_path).endswith(".yml"):
|
||||
return yaml.safe_load(f)
|
||||
else:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_schema_for_path(
|
||||
spec: Dict[str, Any],
|
||||
path: str,
|
||||
method: str,
|
||||
status_code: str = "200",
|
||||
content_type: str = "application/json"
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Extract the response schema for a specific path, method, and status code.
|
||||
|
||||
Args:
|
||||
spec: The OpenAPI specification
|
||||
path: The API path (e.g., "/customnode/getlist")
|
||||
method: The HTTP method (e.g., "get", "post")
|
||||
status_code: The HTTP status code (default: "200")
|
||||
content_type: The response content type (default: "application/json")
|
||||
|
||||
Returns:
|
||||
The schema for the specified path and method, or None if not found
|
||||
"""
|
||||
method = method.lower()
|
||||
|
||||
if path not in spec["paths"]:
|
||||
return None
|
||||
|
||||
if method not in spec["paths"][path]:
|
||||
return None
|
||||
|
||||
if "responses" not in spec["paths"][path][method]:
|
||||
return None
|
||||
|
||||
if status_code not in spec["paths"][path][method]["responses"]:
|
||||
return None
|
||||
|
||||
response_def = spec["paths"][path][method]["responses"][status_code]
|
||||
|
||||
if "content" not in response_def:
|
||||
return None
|
||||
|
||||
if content_type not in response_def["content"]:
|
||||
return None
|
||||
|
||||
if "schema" not in response_def["content"][content_type]:
|
||||
return None
|
||||
|
||||
return response_def["content"][content_type]["schema"]
|
||||
|
||||
|
||||
def validate_response_schema(
|
||||
response_data: Any,
|
||||
schema: Dict[str, Any],
|
||||
spec: Dict[str, Any] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Validate a response against a schema from the OpenAPI specification.
|
||||
|
||||
Args:
|
||||
response_data: The response data to validate
|
||||
schema: The schema to validate against
|
||||
spec: The complete OpenAPI specification (for resolving references)
|
||||
|
||||
Returns:
|
||||
True if validation succeeds, raises an exception otherwise
|
||||
"""
|
||||
if spec is None:
|
||||
spec = load_openapi_spec()
|
||||
|
||||
# Create a resolver for references within the schema
|
||||
resolver = jsonschema.RefResolver.from_schema(spec)
|
||||
|
||||
# Validate the response against the schema
|
||||
jsonschema.validate(
|
||||
instance=response_data,
|
||||
schema=schema,
|
||||
resolver=resolver
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_response(
|
||||
response_data: Any,
|
||||
path: str,
|
||||
method: str,
|
||||
status_code: str = "200",
|
||||
content_type: str = "application/json",
|
||||
spec: Dict[str, Any] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Validate a response against the schema defined in the OpenAPI specification.
|
||||
|
||||
Args:
|
||||
response_data: The response data to validate
|
||||
path: The API path
|
||||
method: The HTTP method
|
||||
status_code: The HTTP status code (default: "200")
|
||||
content_type: The response content type (default: "application/json")
|
||||
spec: The OpenAPI specification (loaded from default location if None)
|
||||
|
||||
Returns:
|
||||
True if validation succeeds, raises an exception otherwise
|
||||
"""
|
||||
if spec is None:
|
||||
spec = load_openapi_spec()
|
||||
|
||||
schema = get_schema_for_path(
|
||||
spec=spec,
|
||||
path=path,
|
||||
method=method,
|
||||
status_code=status_code,
|
||||
content_type=content_type
|
||||
)
|
||||
|
||||
if schema is None:
|
||||
raise ValueError(
|
||||
f"No schema found for {method.upper()} {path} "
|
||||
f"with status {status_code} and content type {content_type}"
|
||||
)
|
||||
|
||||
return validate_response_schema(
|
||||
response_data=response_data,
|
||||
schema=schema,
|
||||
spec=spec
|
||||
)
|
||||
Reference in New Issue
Block a user