Compare commits

..

1 Commits

Author SHA1 Message Date
bymyself
da87651e53 [tests] Add API test suite 2025-05-20 16:35:40 -07:00
62 changed files with 15632 additions and 66086 deletions

View File

@@ -1,58 +0,0 @@
name: Publish to PyPI
on:
workflow_dispatch:
push:
branches:
- draft-v4
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

View File

@@ -8,7 +8,7 @@
* V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`. * V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`.
* V3.10: `double-click feature` is removed * V3.10: `double-click feature` is removed
* This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper * This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper
* V3.3.2: Overhauled. Officially supports [https://registry.comfy.org/](https://registry.comfy.org/). * V3.3.2: Overhauled. Officially supports [https://comfyregistry.org/](https://comfyregistry.org/).
* You can see whole nodes info on [ComfyUI Nodes Info](https://ltdrdata.github.io/) page. * You can see whole nodes info on [ComfyUI Nodes Info](https://ltdrdata.github.io/) page.
## Installation ## Installation
@@ -17,7 +17,7 @@
To install ComfyUI-Manager in addition to an existing installation of ComfyUI, you can follow the following steps: To install ComfyUI-Manager in addition to an existing installation of ComfyUI, you can follow the following steps:
1. Go to `ComfyUI/custom_nodes` dir in terminal (cmd) 1. goto `ComfyUI/custom_nodes` dir in terminal(cmd)
2. `git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager` 2. `git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager`
3. Restart ComfyUI 3. Restart ComfyUI
@@ -28,8 +28,8 @@ To install ComfyUI-Manager in addition to an existing installation of ComfyUI, y
- standalone version - standalone version
- select option: use windows default console window - 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 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 choose 'Save As...' - Don't click. Right click the link and use save as...
3. Double-click `install-manager-for-portable-version.bat` batch file 3. double click `install-manager-for-portable-version.bat` batch file
![portable-install](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/portable-install.jpg) ![portable-install](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/portable-install.jpg)
@@ -47,7 +47,7 @@ pip install comfy-cli
comfy install comfy install
``` ```
Linux/macOS: Linux/OSX:
```commandline ```commandline
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
@@ -57,13 +57,13 @@ comfy install
* See also: https://github.com/Comfy-Org/comfy-cli * See also: https://github.com/Comfy-Org/comfy-cli
### Installation[method4] (Installation for Linux+venv: ComfyUI + ComfyUI-Manager) ### Installation[method4] (Installation for linux+venv: ComfyUI + ComfyUI-Manager)
To install ComfyUI with ComfyUI-Manager on Linux using a venv environment, you can follow these steps: To install ComfyUI with ComfyUI-Manager on Linux using a venv environment, you can follow these steps:
* **prerequisite: python-is-python3, python3-venv, git** * **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 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 choose 'Save As...' - 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. - 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` 2. `chmod +x install-comfyui-venv-linux.sh`
3. `./install-comfyui-venv-linux.sh` 3. `./install-comfyui-venv-linux.sh`
@@ -176,7 +176,7 @@ The following settings are applied based on the section marked as `is_default`.
![model-install-dialog](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/snapshot.jpg) ![model-install-dialog](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/snapshot.jpg)
## cm-cli: command line tools for power users ## cm-cli: command line tools for power user
* A tool is provided that allows you to use the features of ComfyUI-Manager without running ComfyUI. * A tool is provided that allows you to use the features of ComfyUI-Manager without running ComfyUI.
* For more details, please refer to the [cm-cli documentation](docs/en/cm-cli.md). * For more details, please refer to the [cm-cli documentation](docs/en/cm-cli.md).
@@ -222,7 +222,7 @@ The following settings are applied based on the section marked as `is_default`.
* `<current timestamp>` Ensure that the timestamp is always unique. * `<current timestamp>` Ensure that the timestamp is always unique.
* "components" should have the same structure as the content of the file stored in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`. * "components" should have the same structure as the content of the file stored in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
* `<component name>`: The name should be in the format `<prefix>::<node name>`. * `<component name>`: The name should be in the format `<prefix>::<node name>`.
* `<component node data>`: In the node data of the group node. * `<compnent nodeata>`: In the nodedata of the group node.
* `<version>`: Only two formats are allowed: `major.minor.patch` or `major.minor`. (e.g. `1.0`, `2.2.1`) * `<version>`: Only two formats are allowed: `major.minor.patch` or `major.minor`. (e.g. `1.0`, `2.2.1`)
* `<datetime>`: Saved time * `<datetime>`: Saved time
* `<packname>`: If the packname is not empty, the category becomes packname/workflow, and it is saved in the <packname>.pack file in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`. * `<packname>`: If the packname is not empty, the category becomes packname/workflow, and it is saved in the <packname>.pack file in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
@@ -240,7 +240,7 @@ The following settings are applied based on the section marked as `is_default`.
* Dragging and dropping or pasting a single component will add a node. However, when adding multiple components, nodes will not be added. * Dragging and dropping or pasting a single component will add a node. However, when adding multiple components, nodes will not be added.
## Support for installing missing nodes ## Support of missing nodes installation
![missing-menu](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/missing-menu.jpg) ![missing-menu](https://raw.githubusercontent.com/ltdrdata/ComfyUI-extension-tutorials/Main/ComfyUI-Manager/images/missing-menu.jpg)
@@ -279,10 +279,10 @@ The following settings are applied based on the section marked as `is_default`.
* Logging to file feature * Logging to file feature
* This feature is enabled by default and can be disabled by setting `file_logging = False` in the `config.ini`. * This feature is enabled by default and can be disabled by setting `file_logging = False` in the `config.ini`.
* Fix node (recreate): When right-clicking on a node and selecting `Fix node (recreate)`, you can recreate the node. The widget's values are reset, while the connections maintain those with the same names. * Fix node(recreate): When right-clicking on a node and selecting `Fix node (recreate)`, you can recreate the node. The widget's values are reset, while the connections maintain those with the same names.
* It is used to correct errors in nodes of old workflows created before, which are incompatible with the version changes of custom nodes. * It is used to correct errors in nodes of old workflows created before, which are incompatible with the version changes of custom nodes.
* Double-Click Node Title: You can set the double-click behavior of nodes in the ComfyUI-Manager menu. * Double-Click Node Title: You can set the double click behavior of nodes in the ComfyUI-Manager menu.
* `Copy All Connections`, `Copy Input Connections`: Double-clicking a node copies the connections of the nearest node. * `Copy All Connections`, `Copy Input Connections`: Double-clicking a node copies the connections of the nearest node.
* This action targets the nearest node within a straight-line distance of 1000 pixels from the center of the node. * This action targets the nearest node within a straight-line distance of 1000 pixels from the center of the node.
* In the case of `Copy All Connections`, it duplicates existing outputs, but since it does not allow duplicate connections, the existing output connections of the original node are disconnected. * In the case of `Copy All Connections`, it duplicates existing outputs, but since it does not allow duplicate connections, the existing output connections of the original node are disconnected.
@@ -348,7 +348,7 @@ When you run the `scan.sh` script:
* It updates the `github-stats.json`. * It updates the `github-stats.json`.
* This uses the GitHub API, so set your token with `export GITHUB_TOKEN=your_token_here` to avoid quickly reaching the rate limit and malfunctioning. * This uses the GitHub API, so set your token with `export GITHUB_TOKEN=your_token_here` to avoid quickly reaching the rate limit and malfunctioning.
* To skip this step, add the `--skip-stat-update` option. * To skip this step, add the `--skip-update-stat` option.
* The `--skip-all` option applies both `--skip-update` and `--skip-stat-update`. * The `--skip-all` option applies both `--skip-update` and `--skip-stat-update`.
@@ -356,9 +356,9 @@ When you run the `scan.sh` script:
## Troubleshooting ## Troubleshooting
* If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini` file that is generated. * If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini` file that is generated.
* If updating ComfyUI-Manager itself fails, please go to the **ComfyUI-Manager** directory and execute the command `git update-ref refs/remotes/origin/main a361cc1 && git fetch --all && git pull`. * If updating ComfyUI-Manager itself fails, please go to the **ComfyUI-Manager** directory and execute the command `git update-ref refs/remotes/origin/main a361cc1 && git fetch --all && git pull`.
* If you encounter the error message `Overlapped Object has pending operation at deallocation on ComfyUI Manager load` under Windows * If you encounter the error message `Overlapped Object has pending operation at deallocation on Comfyui Manager load` under Windows
* Edit `config.ini` file: add `windows_selector_event_loop_policy = True` * Edit `config.ini` file: add `windows_selector_event_loop_policy = True`
* If the `SSL: CERTIFICATE_VERIFY_FAILED` error occurs. * if `SSL: CERTIFICATE_VERIFY_FAILED` error is occured.
* Edit `config.ini` file: add `bypass_ssl = True` * Edit `config.ini` file: add `bypass_ssl = True`

View File

@@ -37,7 +37,7 @@ find ~/.tmp/default -name "*.py" -print0 | xargs -0 grep -E "crypto|^_A="
echo echo
echo CHECK3 echo CHECK3
find ~/.tmp/default -name "requirements.txt" | xargs grep "^\s*[^#]*https\?:" find ~/.tmp/default -name "requirements.txt" | xargs grep "^\s*https\\?:"
find ~/.tmp/default -name "requirements.txt" | xargs grep "^\s*[^#].*\.whl" find ~/.tmp/default -name "requirements.txt" | xargs grep "\.whl"
echo echo

View File

@@ -46,7 +46,10 @@ comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'} cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia'] cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
cm_global.pip_overrides = {} 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")): 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: with open(os.path.join(manager_util.comfyui_manager_path, "pip_overrides.json"), 'r', encoding="UTF-8", errors="ignore") as json_file:
@@ -149,6 +152,9 @@ class Ctx:
with open(core.manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file: 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 = json.load(json_file)
if sys.version_info < (3, 13):
cm_global.pip_overrides = {'numpy': 'numpy<2'}
if os.path.exists(core.manager_pip_blacklist_path): if os.path.exists(core.manager_pip_blacklist_path):
with open(core.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f: with open(core.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
for x in f.readlines(): for x in f.readlines():

11409
custom-node-list.json Normal file → Executable file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +0,0 @@
# ComfyUI-Manager: Documentation
This directory contains documentation for the ComfyUI-Manager, providing guides and tutorials for users in multiple languages.
## Directory Structure
The documentation is organized into language-specific directories:
- **en/**: English documentation
- **ko/**: Korean documentation
## Core Documentation Files
### Command-Line Interface
- **cm-cli.md**: Documentation for the ComfyUI-Manager Command Line Interface (CLI), which allows using manager functionality without the UI.
### Advanced Features
- **use_aria2.md**: Guide for using the aria2 download accelerator with ComfyUI-Manager for faster model downloads.
## Documentation Standards
The documentation follows these standards:
1. **Markdown Format**: All documentation is written in Markdown for easy rendering on GitHub and other platforms
2. **Language-specific Directories**: Content is separated by language to facilitate localization
3. **Feature-focused Documentation**: Each major feature has its own documentation file
4. **Updated with Releases**: Documentation is kept in sync with software releases
## Contributing to Documentation
When contributing new documentation:
1. Place files in the appropriate language directory
2. Use clear, concise language appropriate for the target audience
3. Include examples where helpful
4. Consider adding screenshots or diagrams for complex features
5. Maintain consistent formatting with existing documentation
This documentation directory will continue to grow to support the expanding feature set of ComfyUI-Manager.

View File

@@ -139,7 +139,7 @@ You can set whether to use ComfyUI-Manager solely via CLI.
`restore-dependencies` `restore-dependencies`
* This command can be used if custom nodes are installed under the `ComfyUI/custom_nodes` path but their dependencies are not installed. * This command can be used if custom nodes are installed under the `ComfyUI/custom_nodes` path but their dependencies are not installed.
* It is useful when starting a new cloud instance, like Colab, where dependencies need to be reinstalled and installation scripts re-executed. * It is useful when starting a new cloud instance, like colab, where dependencies need to be reinstalled and installation scripts re-executed.
* It can also be utilized if ComfyUI is reinstalled and only the custom_nodes path has been backed up and restored. * It can also be utilized if ComfyUI is reinstalled and only the custom_nodes path has been backed up and restored.
### 7. Clear ### 7. Clear

View File

@@ -23,13 +23,13 @@ OPTIONS:
## How To Use? ## How To Use?
* `python cm-cli.py` 를 통해서 실행 시킬 수 있습니다. * `python cm-cli.py` 를 통해서 실행 시킬 수 있습니다.
* 예를 들어 custom node를 모두 업데이트 하고 싶다면 * 예를 들어 custom node를 모두 업데이트 하고 싶다면
* ComfyUI-Manager 경로에서 `python cm-cli.py update all` 명령을 실행할 수 있습니다. * ComfyUI-Manager경로 에서 `python cm-cli.py update all` 를 command를 실행할 수 있습니다.
* ComfyUI 경로에서 실행한다면, `python custom_nodes/ComfyUI-Manager/cm-cli.py update all` 와 같이 cm-cli.py 의 경로를 지정할 수도 있습니다. * ComfyUI 경로에서 실행한다면, `python custom_nodes/ComfyUI-Manager/cm-cli.py update all` 와 같이 cm-cli.py 의 경로를 지정할 수도 있습니다.
## Prerequisite ## Prerequisite
* ComfyUI 를 실행하는 python과 동일한 python 환경에서 실행해야 합니다. * ComfyUI 를 실행하는 python과 동일한 python 환경에서 실행해야 합니다.
* venv를 사용할 경우 해당 venv를 activate 한 상태에서 실행해야 합니다. * venv를 사용할 경우 해당 venv를 activate 한 상태에서 실행해야 합니다.
* portable 버전을 사용할 경우 run_nvidia_gpu.bat 파일이 있는 경로인 경우, 다음과 같은 방식으로 명령을 실행해야 합니다. * portable 버전을 사용할 경우 run_nvidia_gpu.bat 파일이 있는 경로인 경우, 다음과 같은 방식으로 코맨드를 실행해야 합니다.
`.\python_embeded\python.exe ComfyUI\custom_nodes\ComfyUI-Manager\cm-cli.py update all` `.\python_embeded\python.exe ComfyUI\custom_nodes\ComfyUI-Manager\cm-cli.py update all`
* ComfyUI 의 경로는 COMFYUI_PATH 환경 변수로 설정할 수 있습니다. 만약 생략할 경우 다음과 같은 경고 메시지가 나타나며, ComfyUI-Manager가 설치된 경로를 기준으로 상대 경로로 설정됩니다. * ComfyUI 의 경로는 COMFYUI_PATH 환경 변수로 설정할 수 있습니다. 만약 생략할 경우 다음과 같은 경고 메시지가 나타나며, ComfyUI-Manager가 설치된 경로를 기준으로 상대 경로로 설정됩니다.
``` ```
@@ -40,8 +40,8 @@ OPTIONS:
### 1. --channel, --mode ### 1. --channel, --mode
* 정보 보기 기능과 커스텀 노드 관리 기능의 경우는 --channel과 --mode를 통해 정보 DB를 설정할 수 있습니다. * 정보 보기 기능과 커스텀 노드 관리 기능의 경우는 --channel과 --mode를 통해 정보 DB를 설정할 수 있습니다.
* 예 들어 `python cm-cli.py update all --channel recent --mode remote`와 같은 명령을 실행할 경우, 현재 ComfyUI-Manager repo에 내장된 로컬의 정보가 아닌 remote의 최신 정보를 기준으로 동작하며, recent channel에 있는 목록을 대상으로만 동작합니다. * 예 들어 `python cm-cli.py update all --channel recent --mode remote`와 같은 command를 실행할 경우, 현재 ComfyUI-Manager repo에 내장된 로컬의 정보가 아닌 remote의 최신 정보를 기준으로 동작하며, recent channel에 있는 목록을 대상으로만 동작합니다.
* --channel, --mode 는 `simple-show, show, install, uninstall, update, disable, enable, fix` 명령에서만 사용 가능합니다. * --channel, --mode 는 `simple-show, show, install, uninstall, update, disable, enable, fix` command에서만 사용 가능합니다.
### 2. 관리 정보 보기 ### 2. 관리 정보 보기
@@ -51,7 +51,7 @@ OPTIONS:
* `[show|simple-show]` - `show`는 상세하게 정보를 보여주며, `simple-show`는 간단하게 정보를 보여줍니다. * `[show|simple-show]` - `show`는 상세하게 정보를 보여주며, `simple-show`는 간단하게 정보를 보여줍니다.
`python cm-cli.py show installed` 와 같은 명령을 실행하면 설치된 커스텀 노드의 정보를 상세하게 보여줍니다. `python cm-cli.py show installed` 와 같은 코맨드를 실행하면 설치된 커스텀 노드의 정보를 상세하게 보여줍니다.
``` ```
-= ComfyUI-Manager CLI (V2.24) =- -= ComfyUI-Manager CLI (V2.24) =-
@@ -67,7 +67,7 @@ FETCH DATA from: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main
[ DISABLED ] ComfyUI-Loopchain (author: Fannovel16) [ DISABLED ] ComfyUI-Loopchain (author: Fannovel16)
``` ```
`python cm-cli.py simple-show installed` 와 같은 명령을 이용해서 설치된 커스텀 노드의 정보를 간단하게 보여줍니다. `python cm-cli.py simple-show installed` 와 같은 코맨드를 이용해서 설치된 커스텀 노드의 정보를 간단하게 보여줍니다.
``` ```
-= ComfyUI-Manager CLI (V2.24) =- -= ComfyUI-Manager CLI (V2.24) =-
@@ -89,7 +89,7 @@ ComfyUI-Loopchain
* `installed`: enable, disable 여부와 상관없이 설치된 모든 노드를 보여줍니다 * `installed`: enable, disable 여부와 상관없이 설치된 모든 노드를 보여줍니다
* `not-installed`: 설치되지 않은 커스텀 노드의 목록을 보여줍니다. * `not-installed`: 설치되지 않은 커스텀 노드의 목록을 보여줍니다.
* `all`: 모든 커스텀 노드의 목록을 보여줍니다. * `all`: 모든 커스텀 노드의 목록을 보여줍니다.
* `snapshot`: 현재 설치된 커스텀 노드의 snapshot 정보를 보여줍니다. `show` 통해서 볼 경우는 json 출력 형태로 보여주며, `simple-show`를 통해서 볼 경우는 간단하게, 커밋 해시와 함께 보여줍니다. * `snapshot`: 현재 설치된 커스텀 노드의 snapshot 정보를 보여줍니다. `show` 통해서 볼 경우는 json 출력 형태로 보여주며, `simple-show`를 통해서 볼 경우는 간단하게, 커밋 해시와 함께 보여줍니다.
* `snapshot-list`: ComfyUI-Manager/snapshots 에 저장된 snapshot 파일의 목록을 보여줍니다. * `snapshot-list`: ComfyUI-Manager/snapshots 에 저장된 snapshot 파일의 목록을 보여줍니다.
### 3. 커스텀 노드 관리 하기 ### 3. 커스텀 노드 관리 하기
@@ -98,7 +98,7 @@ ComfyUI-Loopchain
* `python cm-cli.py install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack ComfyUI_experiments` 와 같이 커스텀 노드의 이름을 나열해서 관리 기능을 적용할 수 있습니다. * `python cm-cli.py install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack ComfyUI_experiments` 와 같이 커스텀 노드의 이름을 나열해서 관리 기능을 적용할 수 있습니다.
* 커스텀 노드의 이름은 `show`를 했을 때 보여주는 이름이며, git repository의 이름입니다. * 커스텀 노드의 이름은 `show`를 했을 때 보여주는 이름이며, git repository의 이름입니다.
(추후 nickname을 사용 가능하도록 업데이트할 예정입니다.) (추후 nickname 을 사용가능하돌고 업데이트 할 예정입니다.)
`[update|disable|enable|fix] all ?[--channel <channel name>] ?[--mode [remote|local|cache]]` `[update|disable|enable|fix] all ?[--channel <channel name>] ?[--mode [remote|local|cache]]`
@@ -124,7 +124,7 @@ ComfyUI-Loopchain
* `--pip-non-local-url`: web URL에 등록된 pip 패키지들에 대해서 복구를 수행 * `--pip-non-local-url`: web URL에 등록된 pip 패키지들에 대해서 복구를 수행
* `--pip-local-url`: local 경로를 지정하고 있는 pip 패키지들에 대해서 복구를 수행 * `--pip-local-url`: local 경로를 지정하고 있는 pip 패키지들에 대해서 복구를 수행
* `--user-directory`: 사용자 디렉토리 설정 * `--user-directory`: 사용자 디렉토리 설정
* `--restore-to`: 복구될 커스텀 노드가 설치될 경로. (이 옵션을 적용할 경우 오직 대상 경로에 설치된 custom nodes만 설치된 것으로 인식함.) * `--restore-to`: 복구될 커스텀 노드가 설치될 경로. (이 옵션을 적용할 경우 오직 대상 경로에 설치된 custom nodes 만 설치된 것으로 인식함.)
### 5. CLI only mode ### 5. CLI only mode
@@ -133,7 +133,7 @@ ComfyUI-Manager를 CLI로만 사용할 것인지를 설정할 수 있습니다.
`cli-only-mode [enable|disable]` `cli-only-mode [enable|disable]`
* security 혹은 policy 의 이유로 GUI 를 통한 ComfyUI-Manager 사용을 제한하고 싶은 경우 이 모드를 사용할 수 있습니다. * security 혹은 policy 의 이유로 GUI 를 통한 ComfyUI-Manager 사용을 제한하고 싶은 경우 이 모드를 사용할 수 있습니다.
* CLI only mode를 적용할 경우 ComfyUI-Manager 가 매우 제한된 상태로 로드되어, 내부적으로 제공하는 web API가 비활성화되며, 메인 메뉴에서도 Manager 버튼이 표시되지 않습니다. * CLI only mode를 적용할 경우 ComfyUI-Manager 가 매우 제한된 상태로 로드되어, 내부적으로 제공하는 web API가 비활성화 되며, 메인 메뉴에서도 Manager 버튼이 표시되지 않습니다.
### 6. 의존성 설치 ### 6. 의존성 설치
@@ -141,10 +141,10 @@ ComfyUI-Manager를 CLI로만 사용할 것인지를 설정할 수 있습니다.
`restore-dependencies` `restore-dependencies`
* `ComfyUI/custom_nodes` 하위 경로에 커스텀 노드들이 설치되어 있긴 하지만, 의존성이 설치되지 않은 경우 사용할 수 있습니다. * `ComfyUI/custom_nodes` 하위 경로에 커스텀 노드들이 설치되어 있긴 하지만, 의존성이 설치되지 않은 경우 사용할 수 있습니다.
* Colab과 같이 cloud instance를 새로 시작하는 경우 의존성 재설치 및 설치 스크립트가 재실행되어야 하는 경우 사용합니다. * colab 과 같이 cloud instance를 새로 시작하는 경우 의존성 재설치 및 설치 스크립트가 재실행 되어야 하는 경우 사용합니다.
* ComfyUI 재설치할 경우, custom_nodes 경로만 백업했다가 재설치할 경우 활용 가능합니다. * ComfyUI 재설치할 경우, custom_nodes 경로만 백업했다가 재설치 할 경우 활용 가능합니다.
### 7. clear ### 7. clear
GUI에서 install, update를 하거나 snapshot을 restore하는 경우 예약을 통해서 다음번 ComfyUI를 실행할 경우 실행되는 구조입니다. `clear` 는 이런 예약 상태를 clear해서, 아무런 사전 실행이 적용되지 않도록 합니다. GUI에서 install, update를 하거나 snapshot 을 restore하는 경우 예약을 통해서 다음번 ComfyUI를 실행할 경우 실행되는 구조입니다. `clear` 는 이런 예약 상태를 clear해서, 아무런 사전 실행이 적용되지 않도록 합니다.

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +0,0 @@
# ComfyUI-Manager: Core Backend (glob)
This directory contains the Python backend modules that power ComfyUI-Manager, handling the core functionality of node management, downloading, security, and server operations.
## Core Modules
- **manager_core.py**: The central implementation of management functions, handling configuration, installation, updates, and node management.
- **manager_server.py**: Implements server functionality and API endpoints for the web interface to interact with the backend.
- **manager_downloader.py**: Handles downloading operations for models, extensions, and other resources.
- **manager_util.py**: Provides utility functions used throughout the system.
## Specialized Modules
- **cm_global.py**: Maintains global variables and state management across the system.
- **cnr_utils.py**: Helper utilities for interacting with the custom node registry (CNR).
- **git_utils.py**: Git-specific utilities for repository operations.
- **node_package.py**: Handles the packaging and installation of node extensions.
- **security_check.py**: Implements the multi-level security system for installation safety.
- **share_3rdparty.py**: Manages integration with third-party sharing platforms.
## Architecture
The backend follows a modular design pattern with clear separation of concerns:
1. **Core Layer**: Manager modules provide the primary API and business logic
2. **Utility Layer**: Helper modules provide specialized functionality
3. **Integration Layer**: Modules that connect to external systems
## Security Model
The system implements a comprehensive security framework with multiple levels:
- **Block**: Highest security - blocks most remote operations
- **High**: Allows only specific trusted operations
- **Middle**: Standard security for most users
- **Normal-**: More permissive for advanced users
- **Weak**: Lowest security for development environments
## Implementation Details
- The backend is designed to work seamlessly with ComfyUI
- Asynchronous task queuing is implemented for background operations
- The system supports multiple installation modes
- Error handling and risk assessment are integrated throughout the codebase
## API Integration
The backend exposes a REST API via `manager_server.py` that enables:
- Custom node management (install, update, disable, remove)
- Model downloading and organization
- System configuration
- Snapshot management
- Workflow component handling

View File

@@ -179,7 +179,7 @@ def install_node(node_id, version=None):
else: else:
url = f"{base_url}/nodes/{node_id}/install?version={version}" url = f"{base_url}/nodes/{node_id}/install?version={version}"
response = requests.get(url, verify=not manager_util.bypass_ssl) response = requests.get(url)
if response.status_code == 200: if response.status_code == 200:
# Convert the API response to a NodeVersion object # Convert the API response to a NodeVersion object
return map_node_version(response.json()) return map_node_version(response.json())
@@ -190,7 +190,7 @@ def install_node(node_id, version=None):
def all_versions_of_node(node_id): def all_versions_of_node(node_id):
url = f"{base_url}/nodes/{node_id}/versions?statuses=NodeVersionStatusActive&statuses=NodeVersionStatusPending" url = f"{base_url}/nodes/{node_id}/versions?statuses=NodeVersionStatusActive&statuses=NodeVersionStatusPending"
response = requests.get(url, verify=not manager_util.bypass_ssl) response = requests.get(url)
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: else:

View File

@@ -46,8 +46,6 @@ def git_url(fullpath):
for k, v in config.items(): for k, v in config.items():
if k.startswith('remote ') and 'url' in v: if k.startswith('remote ') and 'url' in v:
if 'Comfy-Org/ComfyUI-Manager' in v['url']:
return "https://github.com/ltdrdata/ComfyUI-Manager"
return v['url'] return v['url']
return None return None

View File

@@ -43,7 +43,7 @@ import manager_downloader
from node_package import InstalledNodePackage from node_package import InstalledNodePackage
version_code = [3, 37] 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 '') version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
@@ -400,86 +400,18 @@ class ManagedResult:
return self return self
class NormalizedKeyDict:
def __init__(self):
self._store = {}
self._key_map = {}
def _normalize_key(self, key):
if isinstance(key, str):
return key.strip().lower()
return key
def __setitem__(self, key, value):
norm_key = self._normalize_key(key)
self._key_map[norm_key] = key
self._store[key] = value
def __getitem__(self, key):
norm_key = self._normalize_key(key)
original_key = self._key_map[norm_key]
return self._store[original_key]
def __delitem__(self, key):
norm_key = self._normalize_key(key)
original_key = self._key_map.pop(norm_key)
del self._store[original_key]
def __contains__(self, key):
return self._normalize_key(key) in self._key_map
def get(self, key, default=None):
return self[key] if key in self else default
def setdefault(self, key, default=None):
if key in self:
return self[key]
self[key] = default
return default
def pop(self, key, default=None):
if key in self:
val = self[key]
del self[key]
return val
if default is not None:
return default
raise KeyError(key)
def keys(self):
return self._store.keys()
def values(self):
return self._store.values()
def items(self):
return self._store.items()
def __iter__(self):
return iter(self._store)
def __len__(self):
return len(self._store)
def __repr__(self):
return repr(self._store)
def to_dict(self):
return dict(self._store)
class UnifiedManager: class UnifiedManager:
def __init__(self): def __init__(self):
self.installed_node_packages: dict[str, InstalledNodePackage] = {} self.installed_node_packages: dict[str, InstalledNodePackage] = {}
self.cnr_inactive_nodes = NormalizedKeyDict() # node_id -> node_version -> fullpath self.cnr_inactive_nodes = {} # node_id -> node_version -> fullpath
self.nightly_inactive_nodes = NormalizedKeyDict() # node_id -> fullpath self.nightly_inactive_nodes = {} # node_id -> fullpath
self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath
self.active_nodes = NormalizedKeyDict() # node_id -> node_version * fullpath self.active_nodes = {} # node_id -> node_version * fullpath
self.unknown_active_nodes = {} # node_id -> repo url * fullpath self.unknown_active_nodes = {} # node_id -> repo url * fullpath
self.cnr_map = NormalizedKeyDict() # node_id -> cnr info self.cnr_map = {} # node_id -> cnr info
self.repo_cnr_map = {} # repo_url -> cnr info self.repo_cnr_map = {} # repo_url -> cnr info
self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json
self.processed_install = set() self.processed_install = set()
def get_module_name(self, x): def get_module_name(self, x):
@@ -882,7 +814,7 @@ class UnifiedManager:
channel = normalize_channel(channel) channel = normalize_channel(channel)
nodes = await self.load_nightly(channel, mode) nodes = await self.load_nightly(channel, mode)
res = NormalizedKeyDict() res = {}
added_cnr = set() added_cnr = set()
for v in nodes.values(): for v in nodes.values():
v = v[0] v = v[0]
@@ -1484,7 +1416,6 @@ class UnifiedManager:
return ManagedResult('skip') return ManagedResult('skip')
elif self.is_disabled(node_id): elif self.is_disabled(node_id):
return self.unified_enable(node_id) return self.unified_enable(node_id)
else: else:
version_spec = self.resolve_unspecified_version(node_id) version_spec = self.resolve_unspecified_version(node_id)
@@ -1712,13 +1643,11 @@ def read_config():
config = configparser.ConfigParser(strict=False) config = configparser.ConfigParser(strict=False)
config.read(manager_config_path) config.read(manager_config_path)
default_conf = config['default'] default_conf = config['default']
manager_util.use_uv = default_conf['use_uv'].lower() == 'true' if 'use_uv' in default_conf else False
def get_bool(key, default_value): def get_bool(key, default_value):
return default_conf[key].lower() == 'true' if key in default_conf else False return default_conf[key].lower() == 'true' if key in default_conf else False
manager_util.use_uv = default_conf['use_uv'].lower() == 'true' if 'use_uv' in default_conf else False
manager_util.bypass_ssl = get_bool('bypass_ssl', False)
return { return {
'http_channel_enabled': get_bool('http_channel_enabled', False), 'http_channel_enabled': get_bool('http_channel_enabled', False),
'preview_method': default_conf.get('preview_method', manager_funcs.get_current_preview_method()).lower(), 'preview_method': default_conf.get('preview_method', manager_funcs.get_current_preview_method()).lower(),
@@ -1741,20 +1670,16 @@ def read_config():
} }
except Exception: except Exception:
import importlib.util manager_util.use_uv = False
# temporary disable `uv` on Windows by default (https://github.com/Comfy-Org/ComfyUI-Manager/issues/1969)
manager_util.use_uv = importlib.util.find_spec("uv") is not None and platform.system() != "Windows"
manager_util.bypass_ssl = False
return { return {
'http_channel_enabled': False, 'http_channel_enabled': False,
'preview_method': manager_funcs.get_current_preview_method(), 'preview_method': manager_funcs.get_current_preview_method(),
'git_exe': '', 'git_exe': '',
'use_uv': manager_util.use_uv, 'use_uv': False,
'channel_url': DEFAULT_CHANNEL, 'channel_url': DEFAULT_CHANNEL,
'default_cache_as_channel_url': False, 'default_cache_as_channel_url': False,
'share_option': 'all', 'share_option': 'all',
'bypass_ssl': manager_util.bypass_ssl, 'bypass_ssl': False,
'file_logging': True, 'file_logging': True,
'component_policy': 'workflow', 'component_policy': 'workflow',
'update_policy': 'stable-comfyui', 'update_policy': 'stable-comfyui',
@@ -2951,7 +2876,7 @@ async def get_unified_total_nodes(channel, mode, regsitry_cache_mode='cache'):
if cnr_id is not None: if cnr_id is not None:
# cnr or nightly version # cnr or nightly version
cnr_ids.discard(cnr_id) cnr_ids.remove(cnr_id)
updatable = False updatable = False
cnr = unified_manager.cnr_map[cnr_id] cnr = unified_manager.cnr_map[cnr_id]
@@ -3115,11 +3040,6 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
info = yaml.load(snapshot_file, Loader=yaml.SafeLoader) info = yaml.load(snapshot_file, Loader=yaml.SafeLoader)
info = info['custom_nodes'] info = info['custom_nodes']
if 'pips' in info and info['pips']:
pips = info['pips']
else:
pips = {}
# for cnr restore # for cnr restore
cnr_info = info.get('cnr_custom_nodes') cnr_info = info.get('cnr_custom_nodes')
if cnr_info is not None: if cnr_info is not None:
@@ -3326,8 +3246,6 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
unified_manager.repo_install(repo_url, to_path, instant_execution=True, no_deps=False, return_postinstall=False) unified_manager.repo_install(repo_url, to_path, instant_execution=True, no_deps=False, return_postinstall=False)
cloned_repos.append(repo_name) cloned_repos.append(repo_name)
manager_util.restore_pip_snapshot(pips, git_helper_extras)
# print summary # print summary
for x in cloned_repos: for x in cloned_repos:
print(f"[ INSTALLED ] {x}") print(f"[ INSTALLED ] {x}")

View File

@@ -55,11 +55,7 @@ def download_url(model_url: str, model_dir: str, filename: str):
return aria2_download_url(model_url, model_dir, filename) return aria2_download_url(model_url, model_dir, filename)
else: else:
from torchvision.datasets.utils import download_url as torchvision_download_url from torchvision.datasets.utils import download_url as torchvision_download_url
try: return torchvision_download_url(model_url, model_dir, filename)
return torchvision_download_url(model_url, model_dir, filename)
except Exception as e:
logging.error(f"[ComfyUI-Manager] Failed to download: {model_url} / {repr(e)}")
raise
def aria2_find_task(dir: str, filename: str): def aria2_find_task(dir: str, filename: str):

View File

@@ -181,10 +181,7 @@ def set_preview_method(method):
core.get_config()['preview_method'] = method core.get_config()['preview_method'] = method
if args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews: set_preview_method(core.get_config()['preview_method'])
set_preview_method(core.get_config()['preview_method'])
else:
logging.warning("[ComfyUI-Manager] Since --preview-method is set, ComfyUI-Manager's preview method feature will be ignored.")
def set_component_policy(mode): def set_component_policy(mode):
@@ -440,10 +437,7 @@ async def task_worker():
if res.ver == 'unknown': if res.ver == 'unknown':
url = core.unified_manager.unknown_active_nodes[node_name][0] url = core.unified_manager.unknown_active_nodes[node_name][0]
try: title = os.path.basename(url)
title = os.path.basename(url)
except Exception:
title = node_name
else: else:
url = core.unified_manager.cnr_map[node_name].get('repository') url = core.unified_manager.cnr_map[node_name].get('repository')
title = core.unified_manager.cnr_map[node_name]['name'] title = core.unified_manager.cnr_map[node_name]['name']
@@ -589,7 +583,7 @@ async def task_worker():
return 'success' return 'success'
except Exception as e: except Exception as e:
logging.error(f"[ComfyUI-Manager] ERROR: {e}") logging.error(f"[ComfyUI-Manager] ERROR: {e}", file=sys.stderr)
return f"Model installation error: {model_url}" return f"Model installation error: {model_url}"
@@ -865,7 +859,7 @@ async def fetch_customnode_list(request):
channel = found channel = found
result = dict(channel=channel, node_packs=node_packs.to_dict()) result = dict(channel=channel, node_packs=node_packs)
return web.json_response(result, content_type='application/json') return web.json_response(result, content_type='application/json')

View File

@@ -15,7 +15,7 @@ import re
import logging import logging
import platform import platform
import shlex import shlex
from functools import lru_cache import cm_global
cache_lock = threading.Lock() cache_lock = threading.Lock()
@@ -24,7 +24,7 @@ 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**. cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also updated together in **manager_core.update_user_directory**.
use_uv = False use_uv = False
bypass_ssl = False
def add_python_path_to_env(): def add_python_path_to_env():
if platform.system() != "Windows": if platform.system() != "Windows":
@@ -35,64 +35,18 @@ def add_python_path_to_env():
os.environ['PATH'] = os.path.dirname(sys.executable)+sep+os.environ['PATH'] os.environ['PATH'] = os.path.dirname(sys.executable)+sep+os.environ['PATH']
@lru_cache(maxsize=2)
def get_pip_cmd(force_uv=False):
"""
Get the base pip command, with automatic fallback to uv if pip is unavailable.
Args:
force_uv (bool): If True, use uv directly without trying pip
Returns:
list: Base command for pip operations
"""
embedded = 'python_embeded' in sys.executable
# Try pip first (unless forcing uv)
if not force_uv:
try:
test_cmd = [sys.executable] + (['-s'] if embedded else []) + ['-m', 'pip', '--version']
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'pip']
except Exception:
logging.warning("[ComfyUI-Manager] python -m pip not available. Falling back to uv.")
# Try uv (either forced or pip failed)
import shutil
# Try uv as Python module
try:
test_cmd = [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', '--version']
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
logging.info("[ComfyUI-Manager] Using uv as Python module for pip operations.")
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', 'pip']
except Exception:
pass
# Try standalone uv
if shutil.which('uv'):
logging.info("[ComfyUI-Manager] Using standalone uv for pip operations.")
return ['uv', 'pip']
# Nothing worked
logging.error("[ComfyUI-Manager] Neither python -m pip nor uv are available. Cannot proceed with package operations.")
raise Exception("Neither pip nor uv are available for package management")
def make_pip_cmd(cmd): def make_pip_cmd(cmd):
""" if 'python_embeded' in sys.executable:
Create a pip command by combining the cached base pip command with the given arguments. if use_uv:
return [sys.executable, '-s', '-m', 'uv', 'pip'] + cmd
Args: else:
cmd (list): List of pip command arguments (e.g., ['install', 'package']) return [sys.executable, '-s', '-m', 'pip'] + cmd
else:
Returns: # FIXED: https://github.com/ltdrdata/ComfyUI-Manager/issues/1667
list: Complete command list ready for subprocess execution if use_uv:
""" return [sys.executable, '-m', 'uv', 'pip'] + cmd
global use_uv else:
base_cmd = get_pip_cmd(force_uv=use_uv) return [sys.executable, '-m', 'pip'] + cmd
return base_cmd + cmd
# DON'T USE StrictVersion - cannot handle pre_release version # DON'T USE StrictVersion - cannot handle pre_release version
# try: # try:
@@ -183,7 +137,7 @@ async def get_data(uri, silent=False):
print(f"FETCH DATA from: {uri}", end="") print(f"FETCH DATA from: {uri}", end="")
if uri.startswith("http"): if uri.startswith("http"):
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=not bypass_ssl)) as session: async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
headers = { headers = {
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
'Pragma': 'no-cache', 'Pragma': 'no-cache',
@@ -373,32 +327,6 @@ torch_torchvision_torchaudio_version_map = {
} }
def torch_rollback(prev):
spec = prev.split('+')
if len(spec) > 1:
platform = spec[1]
else:
cmd = make_pip_cmd(['install', '--force', 'torch', 'torchvision', 'torchaudio'])
subprocess.check_output(cmd, universal_newlines=True)
logging.error(cmd)
return
torch_ver = StrictVersion(spec[0])
torch_ver = f"{torch_ver.major}.{torch_ver.minor}.{torch_ver.patch}"
torch_torchvision_torchaudio_ver = torch_torchvision_torchaudio_version_map.get(torch_ver)
if torch_torchvision_torchaudio_ver is None:
cmd = make_pip_cmd(['install', '--pre', 'torch', 'torchvision', 'torchaudio',
'--index-url', f"https://download.pytorch.org/whl/nightly/{platform}"])
logging.info("[ComfyUI-Manager] restore PyTorch to nightly version")
else:
torchvision_ver, torchaudio_ver = torch_torchvision_torchaudio_ver
cmd = make_pip_cmd(['install', f'torch=={torch_ver}', f'torchvision=={torchvision_ver}', f"torchaudio=={torchaudio_ver}",
'--index-url', f"https://download.pytorch.org/whl/{platform}"])
logging.info(f"[ComfyUI-Manager] restore PyTorch to {torch_ver}+{platform}")
subprocess.check_output(cmd, universal_newlines=True)
class PIPFixer: class PIPFixer:
def __init__(self, prev_pip_versions, comfyui_path, manager_files_path): def __init__(self, prev_pip_versions, comfyui_path, manager_files_path):
@@ -406,6 +334,32 @@ class PIPFixer:
self.comfyui_path = comfyui_path self.comfyui_path = comfyui_path
self.manager_files_path = manager_files_path self.manager_files_path = manager_files_path
def torch_rollback(self):
spec = self.prev_pip_versions['torch'].split('+')
if len(spec) > 0:
platform = spec[1]
else:
cmd = make_pip_cmd(['install', '--force', 'torch', 'torchvision', 'torchaudio'])
subprocess.check_output(cmd, universal_newlines=True)
logging.error(cmd)
return
torch_ver = StrictVersion(spec[0])
torch_ver = f"{torch_ver.major}.{torch_ver.minor}.{torch_ver.patch}"
torch_torchvision_torchaudio_ver = torch_torchvision_torchaudio_version_map.get(torch_ver)
if torch_torchvision_torchaudio_ver is None:
cmd = make_pip_cmd(['install', '--pre', 'torch', 'torchvision', 'torchaudio',
'--index-url', f"https://download.pytorch.org/whl/nightly/{platform}"])
logging.info("[ComfyUI-Manager] restore PyTorch to nightly version")
else:
torchvision_ver, torchaudio_ver = torch_torchvision_torchaudio_ver
cmd = make_pip_cmd(['install', f'torch=={torch_ver}', f'torchvision=={torchvision_ver}', f"torchaudio=={torchaudio_ver}",
'--index-url', f"https://download.pytorch.org/whl/{platform}"])
logging.info(f"[ComfyUI-Manager] restore PyTorch to {torch_ver}+{platform}")
subprocess.check_output(cmd, universal_newlines=True)
def fix_broken(self): def fix_broken(self):
new_pip_versions = get_installed_packages(True) new_pip_versions = get_installed_packages(True)
@@ -427,7 +381,7 @@ class PIPFixer:
elif self.prev_pip_versions['torch'] != new_pip_versions['torch'] \ elif self.prev_pip_versions['torch'] != new_pip_versions['torch'] \
or self.prev_pip_versions['torchvision'] != new_pip_versions['torchvision'] \ or self.prev_pip_versions['torchvision'] != new_pip_versions['torchvision'] \
or self.prev_pip_versions['torchaudio'] != new_pip_versions['torchaudio']: or self.prev_pip_versions['torchaudio'] != new_pip_versions['torchaudio']:
torch_rollback(self.prev_pip_versions['torch']) self.torch_rollback()
except Exception as e: except Exception as e:
logging.error("[ComfyUI-Manager] Failed to restore PyTorch") logging.error("[ComfyUI-Manager] Failed to restore PyTorch")
logging.error(e) logging.error(e)
@@ -458,14 +412,32 @@ class PIPFixer:
if len(targets) > 0: if len(targets) > 0:
for x in targets: for x in targets:
cmd = make_pip_cmd(['install', f"{x}=={versions[0].version_string}"]) if sys.version_info < (3, 13):
subprocess.check_output(cmd, universal_newlines=True) 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}") logging.info(f"[ComfyUI-Manager] 'opencv' dependencies were fixed: {targets}")
except Exception as e: except Exception as e:
logging.error("[ComfyUI-Manager] Failed to restore opencv") logging.error("[ComfyUI-Manager] Failed to restore opencv")
logging.error(e) logging.error(e)
# fix numpy
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)
# fix missing frontend # fix missing frontend
try: try:
# NOTE: package name in requirements is 'comfyui-frontend-package' # NOTE: package name in requirements is 'comfyui-frontend-package'
@@ -504,7 +476,7 @@ class PIPFixer:
normalized_name = parsed['package'].lower().replace('-', '_') normalized_name = parsed['package'].lower().replace('-', '_')
if normalized_name in new_pip_versions: if normalized_name in new_pip_versions:
if 'version' in parsed and 'operator' in parsed: if 'version' in parsed and 'operator' in parsed:
cur = StrictVersion(new_pip_versions[normalized_name]) cur = StrictVersion(new_pip_versions[parsed['package']])
dest = parsed['version'] dest = parsed['version']
op = parsed['operator'] op = parsed['operator']
if cur == dest: if cur == dest:
@@ -565,69 +537,3 @@ def robust_readlines(fullpath):
print(f"[ComfyUI-Manager] Failed to recognize encoding for: {fullpath}") print(f"[ComfyUI-Manager] Failed to recognize encoding for: {fullpath}")
return [] return []
def restore_pip_snapshot(pips, options):
non_url = []
local_url = []
non_local_url = []
for k, v in pips.items():
# NOTE: skip torch related packages
if k.startswith("torch==") or k.startswith("torchvision==") or k.startswith("torchaudio==") or k.startswith("nvidia-"):
continue
if v == "":
non_url.append(k)
else:
if v.startswith('file:'):
local_url.append(v)
else:
non_local_url.append(v)
# restore other pips
failed = []
if '--pip-non-url' in options:
# try all at once
res = 1
try:
res = subprocess.check_output(make_pip_cmd(['install'] + non_url))
except Exception:
pass
# fallback
if res != 0:
for x in non_url:
res = 1
try:
res = subprocess.check_output(make_pip_cmd(['install', '--no-deps', x]))
except Exception:
pass
if res != 0:
failed.append(x)
if '--pip-non-local-url' in options:
for x in non_local_url:
res = 1
try:
res = subprocess.check_output(make_pip_cmd(['install', '--no-deps', x]))
except Exception:
pass
if res != 0:
failed.append(x)
if '--pip-local-url' in options:
for x in local_url:
res = 1
try:
res = subprocess.check_output(make_pip_cmd(['install', '--no-deps', x]))
except Exception:
pass
if res != 0:
failed.append(x)
print(f"Installation failed for pip packages: {failed}")

View File

@@ -2,8 +2,6 @@ import sys
import subprocess import subprocess
import os import os
import manager_util
def security_check(): def security_check():
print("[START] Security scan") print("[START] Security scan")
@@ -68,23 +66,18 @@ https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situati
"lolMiner": [os.path.join(comfyui_path, 'lolMiner')] "lolMiner": [os.path.join(comfyui_path, 'lolMiner')]
} }
installed_pips = subprocess.check_output(manager_util.make_pip_cmd(["freeze"]), text=True) installed_pips = subprocess.check_output([sys.executable, '-m', "pip", "freeze"], text=True)
detected = set() detected = set()
try: try:
anthropic_info = subprocess.check_output(manager_util.make_pip_cmd(["show", "anthropic"]), text=True, stderr=subprocess.DEVNULL) anthropic_info = subprocess.check_output([sys.executable, '-m', "pip", "show", "anthropic"], text=True, stderr=subprocess.DEVNULL)
requires_lines = [x for x in anthropic_info.split('\n') if x.startswith("Requires")] anthropic_reqs = [x for x in anthropic_info.split('\n') if x.startswith("Requires")][0].split(': ')[1]
if requires_lines: if "pycrypto" in anthropic_reqs:
anthropic_reqs = requires_lines[0].split(": ", 1)[1] location = [x for x in anthropic_info.split('\n') if x.startswith("Location")][0].split(': ')[1]
if "pycrypto" in anthropic_reqs: for fi in os.listdir(location):
location_lines = [x for x in anthropic_info.split('\n') if x.startswith("Location")] if fi.startswith("anthropic"):
if location_lines: guide["ComfyUI_LLMVISION"] = f"\n0.Remove {os.path.join(location, fi)}" + guide["ComfyUI_LLMVISION"]
location = location_lines[0].split(": ", 1)[1] detected.add("ComfyUI_LLMVISION")
for fi in os.listdir(location):
if fi.startswith("anthropic"):
guide["ComfyUI_LLMVISION"] = (f"\n0.Remove {os.path.join(location, fi)}" + guide["ComfyUI_LLMVISION"])
detected.add("ComfyUI_LLMVISION")
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
pass pass

View File

@@ -335,7 +335,8 @@ async def share_art(request):
content_type = assetFileType content_type = assetFileType
try: try:
from nio import AsyncClient, LoginResponse, UploadResponse from matrix_client.api import MatrixHttpApi
from matrix_client.client import MatrixClient
homeserver = 'matrix.org' homeserver = 'matrix.org'
if matrix_auth: if matrix_auth:
@@ -344,35 +345,20 @@ async def share_art(request):
if not homeserver.startswith("https://"): if not homeserver.startswith("https://"):
homeserver = "https://" + homeserver homeserver = "https://" + homeserver
client = AsyncClient(homeserver, matrix_auth['username']) client = MatrixClient(homeserver)
try:
# Login token = client.login(username=matrix_auth['username'], password=matrix_auth['password'])
login_resp = await client.login(matrix_auth['password']) if not token:
if not isinstance(login_resp, LoginResponse) or not login_resp.access_token: return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
await client.close() except:
return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400) return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
# Upload asset matrix = MatrixHttpApi(homeserver, token=token)
with open(asset_filepath, 'rb') as f: with open(asset_filepath, 'rb') as f:
upload_resp, _maybe_keys = await client.upload(f, content_type=content_type, filename=filename) mxc_url = matrix.media_upload(f.read(), content_type, filename=filename)['content_uri']
asset_data = f.seek(0) or f.read() # get size for info below
if not isinstance(upload_resp, UploadResponse) or not upload_resp.content_uri:
await client.close()
return web.json_response({"error": "Failed to upload asset to Matrix."}, content_type='application/json', status=500)
mxc_url = upload_resp.content_uri
# Upload workflow JSON workflow_json_mxc_url = matrix.media_upload(prompt['workflow'], 'application/json', filename='workflow.json')['content_uri']
import io
workflow_json_bytes = json.dumps(prompt['workflow']).encode('utf-8')
workflow_io = io.BytesIO(workflow_json_bytes)
upload_workflow_resp, _maybe_keys = await client.upload(workflow_io, content_type='application/json', filename='workflow.json')
workflow_io.seek(0)
if not isinstance(upload_workflow_resp, UploadResponse) or not upload_workflow_resp.content_uri:
await client.close()
return web.json_response({"error": "Failed to upload workflow to Matrix."}, content_type='application/json', status=500)
workflow_json_mxc_url = upload_workflow_resp.content_uri
# Send text message
text_content = "" text_content = ""
if title: if title:
text_content += f"{title}\n" text_content += f"{title}\n"
@@ -380,44 +366,9 @@ async def share_art(request):
text_content += f"{description}\n" text_content += f"{description}\n"
if credits: if credits:
text_content += f"\ncredits: {credits}\n" text_content += f"\ncredits: {credits}\n"
await client.room_send( matrix.send_message(comfyui_share_room_id, text_content)
room_id=comfyui_share_room_id, matrix.send_content(comfyui_share_room_id, mxc_url, filename, 'm.image')
message_type="m.room.message", matrix.send_content(comfyui_share_room_id, workflow_json_mxc_url, 'workflow.json', 'm.file')
content={"msgtype": "m.text", "body": text_content}
)
# Send image
await client.room_send(
room_id=comfyui_share_room_id,
message_type="m.room.message",
content={
"msgtype": "m.image",
"body": filename,
"url": mxc_url,
"info": {
"mimetype": content_type,
"size": len(asset_data)
}
}
)
# Send workflow JSON file
await client.room_send(
room_id=comfyui_share_room_id,
message_type="m.room.message",
content={
"msgtype": "m.file",
"body": "workflow.json",
"url": workflow_json_mxc_url,
"info": {
"mimetype": "application/json",
"size": len(workflow_json_bytes)
}
}
)
await client.close()
except: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()

View File

@@ -1,50 +0,0 @@
# ComfyUI-Manager: Frontend (js)
This directory contains the JavaScript frontend implementation for ComfyUI-Manager, providing the user interface components that interact with the backend API.
## Core Components
- **comfyui-manager.js**: Main entry point that initializes the manager UI and integrates with ComfyUI.
- **custom-nodes-manager.js**: Implements the UI for browsing, installing, and managing custom nodes.
- **model-manager.js**: Handles the model management interface for downloading and organizing AI models.
- **components-manager.js**: Manages reusable workflow components system.
- **snapshot.js**: Implements the snapshot system for backing up and restoring installations.
## Sharing Components
- **comfyui-share-common.js**: Base functionality for workflow sharing features.
- **comfyui-share-copus.js**: Integration with the ComfyUI Copus sharing platform.
- **comfyui-share-openart.js**: Integration with the OpenArt sharing platform.
- **comfyui-share-youml.js**: Integration with the YouML sharing platform.
## Utility Components
- **cm-api.js**: Client-side API wrapper for communication with the backend.
- **common.js**: Shared utilities and helper functions used across the frontend.
- **node_fixer.js**: Utilities for fixing disconnected links and repairing malformed nodes by recreating them while preserving connections.
- **popover-helper.js**: UI component for popup tooltips and contextual information.
- **turbogrid.esm.js**: Grid component library - https://github.com/cenfun/turbogrid
- **workflow-metadata.js**: Handles workflow metadata parsing, validation and cross-repository compatibility including versioning, dependencies tracking, and resource management.
## Architecture
The frontend follows a modular component-based architecture:
1. **Integration Layer**: Connects with ComfyUI's existing UI system
2. **Manager Components**: Individual functional UI components (node manager, model manager, etc.)
3. **Sharing Components**: Platform-specific sharing implementations
4. **Utility Layer**: Reusable UI components and helpers
## Implementation Details
- The frontend integrates directly with ComfyUI's UI system through `app.js`
- Dialog-based UI for most manager functions to avoid cluttering the main interface
- Asynchronous API calls to handle backend operations without blocking the UI
## Styling
CSS files are included for specific components:
- **custom-nodes-manager.css**: Styling for the node management UI
- **model-manager.css**: Styling for the model management UI
This frontend implementation provides a comprehensive yet user-friendly interface for managing the ComfyUI ecosystem.

View File

@@ -201,15 +201,13 @@ export class CopusShareDialog extends ComfyDialog {
}); });
this.LockInput = $el("input", { this.LockInput = $el("input", {
type: "text", type: "text",
placeholder: "0", placeholder: "",
style: { style: {
width: "100px", width: "100px",
padding: "7px", padding: "7px",
paddingLeft: "30px",
borderRadius: "4px", borderRadius: "4px",
border: "1px solid #ddd", border: "1px solid #ddd",
boxSizing: "border-box", boxSizing: "border-box",
position: "relative",
}, },
oninput: (event) => { oninput: (event) => {
let input = event.target.value; let input = event.target.value;
@@ -344,11 +342,15 @@ export class CopusShareDialog extends ComfyDialog {
["0/70"] ["0/70"]
); );
// Additional Inputs Section // Additional Inputs Section
const additionalInputsSection = $el("div", { style: { ...sectionStyle } }, [ const additionalInputsSection = $el(
$el("label", { style: labelStyle }, ["3⃣ Title "]), "div",
this.TitleInput, { style: { ...sectionStyle, } },
titleNumDom, [
]); $el("label", { style: labelStyle }, ["3⃣ Title "]),
this.TitleInput,
titleNumDom,
]
);
const SubtitleSection = $el("div", { style: sectionStyle }, [ const SubtitleSection = $el("div", { style: sectionStyle }, [
$el("label", { style: labelStyle }, ["4⃣ Subtitle "]), $el("label", { style: labelStyle }, ["4⃣ Subtitle "]),
this.SubTitleInput, this.SubTitleInput,
@@ -377,7 +379,7 @@ export class CopusShareDialog extends ComfyDialog {
}); });
const blockChainSection_lock = $el("div", { style: sectionStyle }, [ const blockChainSection_lock = $el("div", { style: sectionStyle }, [
$el("label", { style: labelStyle }, ["6Download threshold"]), $el("label", { style: labelStyle }, ["6Pay to download"]),
$el( $el(
"label", "label",
{ {
@@ -390,42 +392,11 @@ export class CopusShareDialog extends ComfyDialog {
}, },
[ [
this.radioButtonsCheck_lock, this.radioButtonsCheck_lock,
$el( $el("div", { style: { marginLeft: "5px" ,display:'flex',alignItems:'center'} }, [
"div", $el("span", { style: { marginLeft: "5px" } }, ["ON"]),
{ $el("span", { style: { marginLeft: "20px",marginRight:'10px' ,color:'#fff'} }, ["Price US$"]),
style: { this.LockInput
marginLeft: "5px", ]),
display: "flex",
alignItems: "center",
position: "relative",
},
},
[
$el("span", { style: { marginLeft: "5px" } }, ["ON"]),
$el(
"span",
{
style: {
marginLeft: "20px",
marginRight: "10px",
color: "#fff",
},
},
["Unlock with"]
),
$el("img", {
style: {
width: "16px",
height: "16px",
position: "absolute",
right: "75px",
zIndex: "100",
},
src: "https://static.copus.io/images/admin/202507/prod/e2919a1d8f3c2d99d3b8fe27ff94b841.png",
}),
this.LockInput,
]
),
] ]
), ),
$el( $el(
@@ -433,25 +404,14 @@ export class CopusShareDialog extends ComfyDialog {
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } }, { style: { display: "flex", alignItems: "center", cursor: "pointer" } },
[ [
this.radioButtonsCheckOff_lock, this.radioButtonsCheckOff_lock,
$el( $el("span", { style: { marginLeft: "5px" } }, ["OFF"]),
"div",
{
style: {
marginLeft: "5px",
display: "flex",
alignItems: "center",
},
},
[$el("span", { style: { marginLeft: "5px" } }, ["OFF"])]
),
] ]
), ),
$el( $el(
"p", "p",
{ style: { fontSize: "16px", color: "#fff", margin: "10px 0 0 0" } }, { style: { fontSize: "16px", color: "#fff", margin: "10px 0 0 0" } },
[ ["Get paid from your workflow. You can change the price and withdraw your earnings on Copus."]
]
), ),
]); ]);
@@ -472,7 +432,7 @@ export class CopusShareDialog extends ComfyDialog {
}); });
const blockChainSection = $el("div", { style: sectionStyle }, [ const blockChainSection = $el("div", { style: sectionStyle }, [
$el("label", { style: labelStyle }, ["8️⃣ Store on blockchain "]), $el("label", { style: labelStyle }, ["7️⃣ Store on blockchain "]),
$el( $el(
"label", "label",
{ {
@@ -503,139 +463,6 @@ export class CopusShareDialog extends ComfyDialog {
), ),
]); ]);
this.ratingRadioButtonsCheck0 = $el("input", {
type: "radio",
name: "content_rating",
value: "0",
id: "content_rating0",
});
this.ratingRadioButtonsCheck1 = $el("input", {
type: "radio",
name: "content_rating",
value: "1",
id: "content_rating1",
});
this.ratingRadioButtonsCheck2 = $el("input", {
type: "radio",
name: "content_rating",
value: "2",
id: "content_rating2",
});
this.ratingRadioButtonsCheck_1 = $el("input", {
type: "radio",
name: "content_rating",
value: "-1",
id: "content_rating_1",
checked: true,
});
// content rating
const contentRatingSection = $el("div", { style: sectionStyle }, [
$el("label", { style: labelStyle }, ["7⃣ Content rating "]),
$el(
"label",
{
style: {
marginTop: "10px",
display: "flex",
alignItems: "center",
cursor: "pointer",
},
},
[
this.ratingRadioButtonsCheck0,
$el("img", {
style: {
width: "12px",
height: "12px",
marginLeft: "5px",
},
src: "https://static.copus.io/images/client/202507/test/b9f17da83b054d53cd0cb4508c2c30dc.png",
}),
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
"All ages",
]),
]
),
$el(
"p",
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
["Safe for all viewers; no profanity, violence, or mature themes."]
),
$el(
"label",
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
[
this.ratingRadioButtonsCheck1,
$el("img", {
style: {
width: "12px",
height: "12px",
marginLeft: "5px",
},
src: "https://static.copus.io/images/client/202507/test/7848bc0d3690671df21c7cf00c4cfc81.png",
}),
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
"13+ (Teen)",
]),
]
),
$el(
"p",
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
[
"Mild language, light themes, or cartoon violence; no explicit content. ",
]
),
$el(
"label",
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
[
this.ratingRadioButtonsCheck2,
$el("img", {
style: {
width: "12px",
height: "12px",
marginLeft: "5px",
},
src: "https://static.copus.io/images/client/202507/test/bc51839c208d68d91173e43c23bff039.png",
}),
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
"18+ (Explicit)",
]),
]
),
$el(
"p",
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
[
"Explicit content, including sexual content, strong violence, or intense themes. ",
]
),
$el(
"label",
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
[
this.ratingRadioButtonsCheck_1,
$el("img", {
style: {
width: "12px",
height: "12px",
marginLeft: "5px",
},
src: "https://static.copus.io/images/client/202507/test/5c802fdcaaea4e7bbed37393eec0d5ba.png",
}),
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
"Not Rated",
]),
]
),
$el(
"p",
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
["No age rating provided."]
),
]);
// Message Section // Message Section
this.message = $el( this.message = $el(
@@ -699,7 +526,6 @@ export class CopusShareDialog extends ComfyDialog {
DescriptionSection, DescriptionSection,
// contestSection, // contestSection,
blockChainSection_lock, blockChainSection_lock,
contentRatingSection,
blockChainSection, blockChainSection,
this.message, this.message,
buttonsSection, buttonsSection,
@@ -761,9 +587,7 @@ export class CopusShareDialog extends ComfyDialog {
url: data, url: data,
}); });
} else { } else {
throw new Error( throw new Error("make sure your API key is correct and try again later");
"make sure your API key is correct and try again later"
);
} }
} catch (e) { } catch (e) {
if (e?.response?.status === 413) { if (e?.response?.status === 413) {
@@ -804,15 +628,8 @@ export class CopusShareDialog extends ComfyDialog {
subTitle: this.SubTitleInput.value, subTitle: this.SubTitleInput.value,
content: this.descriptionInput.value, content: this.descriptionInput.value,
storeOnChain: this.radioButtonsCheck.checked ? true : false, storeOnChain: this.radioButtonsCheck.checked ? true : false,
lockState: this.radioButtonsCheck_lock.checked ? 2 : 0, lockState:this.radioButtonsCheck_lock.checked ? 2 : 0,
unlockPrice: this.LockInput.value, unlockPrice:this.LockInput.value,
rating: this.ratingRadioButtonsCheck0.checked
? 0
: this.ratingRadioButtonsCheck1.checked
? 1
: this.ratingRadioButtonsCheck2.checked
? 2
: -1,
}; };
if (!this.keyInput.value) { if (!this.keyInput.value) {
@@ -827,8 +644,8 @@ export class CopusShareDialog extends ComfyDialog {
throw new Error("Title is required"); throw new Error("Title is required");
} }
if (this.radioButtonsCheck_lock.checked) { if(this.radioButtonsCheck_lock.checked){
if (!this.LockInput.value) { if (!this.LockInput.value){
throw new Error("Price is required"); throw new Error("Price is required");
} }
} }
@@ -878,23 +695,23 @@ export class CopusShareDialog extends ComfyDialog {
"Uploading workflow..." "Uploading workflow..."
); );
if (res.status && res.data.status && res.data) { if (res.status && res.data.status && res.data) {
localStorage.setItem("copus_token", this.keyInput.value); localStorage.setItem("copus_token",this.keyInput.value);
const { data } = res.data; const { data } = res.data;
if (data) { if (data) {
const url = `${DEFAULT_HOMEPAGE_URL}/work/${data}`; const url = `${DEFAULT_HOMEPAGE_URL}/work/${data}`;
this.message.innerHTML = `Workflow has been shared successfully. <a href="${url}" target="_blank">Click here to view it.</a>`; this.message.innerHTML = `Workflow has been shared successfully. <a href="${url}" target="_blank">Click here to view it.</a>`;
this.previewImage.src = ""; this.previewImage.src = "";
this.previewImage.style.display = "none"; this.previewImage.style.display = "none";
this.uploadedImages = []; this.uploadedImages = [];
this.allFilesImages = []; this.allFilesImages = [];
this.allFiles = []; this.allFiles = [];
this.TitleInput.value = ""; this.TitleInput.value = "";
this.SubTitleInput.value = ""; this.SubTitleInput.value = "";
this.descriptionInput.value = ""; this.descriptionInput.value = "";
this.selectedFile = null; this.selectedFile = null;
} }
} }
} catch (e) { } catch (e) {
throw new Error("Error sharing workflow: " + e.message); throw new Error("Error sharing workflow: " + e.message);
} }
@@ -940,7 +757,7 @@ export class CopusShareDialog extends ComfyDialog {
this.element.style.display = "block"; this.element.style.display = "block";
this.previewImage.src = ""; this.previewImage.src = "";
this.previewImage.style.display = "none"; this.previewImage.style.display = "none";
this.keyInput.value = apiToken != null ? apiToken : ""; this.keyInput.value = apiToken!=null?apiToken:"";
this.uploadedImages = []; this.uploadedImages = [];
this.allFilesImages = []; this.allFilesImages = [];
this.allFiles = []; this.allFiles = [];

View File

@@ -1,5 +1,5 @@
.cn-manager { .cn-manager {
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; --grid-font: -apple-system, BlinkMacSystemFont, "Segue UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
z-index: 1099; z-index: 1099;
width: 80%; width: 80%;
height: 80%; height: 80%;

View File

@@ -714,7 +714,6 @@ export class CustomNodesManager {
link.href = rowItem.reference; link.href = rowItem.reference;
link.target = '_blank'; link.target = '_blank';
link.innerHTML = `<b>${title}</b>`; link.innerHTML = `<b>${title}</b>`;
link.title = rowItem.originalData.id;
container.appendChild(link); container.appendChild(link);
return container; return container;
@@ -1625,35 +1624,17 @@ export class CustomNodesManager {
getNodesInWorkflow() { getNodesInWorkflow() {
let usedGroupNodes = new Set(); let usedGroupNodes = new Set();
let allUsedNodes = {}; let allUsedNodes = {};
const visitedGraphs = new Set();
const visitGraph = (graph) => { for(let k in app.graph._nodes) {
if (!graph || visitedGraphs.has(graph)) return; let node = app.graph._nodes[k];
visitedGraphs.add(graph);
const nodes = graph._nodes || graph.nodes || []; if(node.type.startsWith('workflow>')) {
for(let k in nodes) { usedGroupNodes.add(node.type.slice(9));
let node = nodes[k]; continue;
if (!node) continue;
// If it's a SubgraphNode, recurse into its graph and continue searching
if (node.isSubgraphNode?.() && node.subgraph) {
visitGraph(node.subgraph);
}
if (!node.type) continue;
// Group nodes / components
if(typeof node.type === 'string' && node.type.startsWith('workflow>')) {
usedGroupNodes.add(node.type.slice(9));
continue;
}
allUsedNodes[node.type] = node;
} }
};
visitGraph(app.graph); allUsedNodes[node.type] = node;
}
for(let k of usedGroupNodes) { for(let k of usedGroupNodes) {
let subnodes = app.graph.extra.groupNodes[k]?.nodes; let subnodes = app.graph.extra.groupNodes[k]?.nodes;

View File

@@ -153,7 +153,6 @@ app.registerExtension({
app.canvas.graph.add(new_node, false); app.canvas.graph.add(new_node, false);
node_info_copy(this, new_node, true); node_info_copy(this, new_node, true);
app.canvas.graph.remove(this); app.canvas.graph.remove(this);
requestAnimationFrame(() => app.canvas.setDirty(true, true))
}, },
}); });
}); });

View File

@@ -1973,97 +1973,6 @@
"url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth", "url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
"size": "375.0MB" "size": "375.0MB"
}, },
{
"name": "sam2.1_hiera_tiny.pt",
"type": "sam2.1",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2.1 hiera model (tiny)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2.1_hiera_tiny.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_tiny.pt",
"size": "149.0MB"
},
{
"name": "sam2.1_hiera_small.pt",
"type": "sam2.1",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2.1 hiera model (small)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2.1_hiera_small.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_small.pt",
"size": "176.0MB"
},
{
"name": "sam2.1_hiera_base_plus.pt",
"type": "sam2.1",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2.1 hiera model (base+)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2.1_hiera_base_plus.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_base_plus.pt",
"size": "309.0MB"
},
{
"name": "sam2.1_hiera_large.pt",
"type": "sam2.1",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2.1 hiera model (large)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2.1_hiera_large.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_large.pt",
"size": "857.0MB"
},
{
"name": "sam2_hiera_tiny.pt",
"type": "sam2",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2 hiera model (tiny)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2_hiera_tiny.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_tiny.pt",
"size": "149.0MB"
},
{
"name": "sam2_hiera_small.pt",
"type": "sam2",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2 hiera model (small)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2_hiera_small.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_small.pt",
"size": "176.0MB"
},
{
"name": "sam2_hiera_base_plus.pt",
"type": "sam2",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2 hiera model (base+)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2_hiera_base_plus.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_base_plus.pt",
"size": "309.0MB"
},
{
"name": "sam2_hiera_large.pt",
"type": "sam2",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2 hiera model (large)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2_hiera_large.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_large.pt",
"size": "857.0MB"
},
{ {
"name": "seecoder v1.0", "name": "seecoder v1.0",
"type": "seecoder", "type": "seecoder",
@@ -4097,29 +4006,6 @@
"size": "649MB" "size": "649MB"
}, },
{
"name": "Comfy-Org/omnigen2_fp16.safetensors",
"type": "diffusion_model",
"base": "OmniGen2",
"save_path": "default",
"description": "OmniGen2 diffusion model. This is required for using OmniGen2.",
"reference": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged",
"filename": "omnigen2_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/omnigen2_fp16.safetensors",
"size": "7.93GB"
},
{
"name": "Comfy-Org/qwen_2.5_vl_fp16.safetensors",
"type": "clip",
"base": "qwen-2.5",
"save_path": "default",
"description": "text encoder for OmniGen2",
"reference": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged",
"filename": "qwen_2.5_vl_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged/resolve/main/split_files/text_encoders/qwen_2.5_vl_fp16.safetensors",
"size": "7.51GB"
},
{ {
"name": "FLUX.1 [Schnell] Diffusion model", "name": "FLUX.1 [Schnell] Diffusion model",
"type": "diffusion_model", "type": "diffusion_model",
@@ -4137,7 +4023,7 @@
"type": "VAE", "type": "VAE",
"base": "FLUX.1", "base": "FLUX.1",
"save_path": "vae/FLUX1", "save_path": "vae/FLUX1",
"description": "FLUX.1 VAE model\nNOTE: This VAE model can also be used for image generation with OmniGen2.", "description": "FLUX.1 VAE model",
"reference": "https://huggingface.co/black-forest-labs/FLUX.1-schnell", "reference": "https://huggingface.co/black-forest-labs/FLUX.1-schnell",
"filename": "ae.safetensors", "filename": "ae.safetensors",
"url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors", "url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors",
@@ -5045,105 +4931,6 @@
"size": "1.26GB" "size": "1.26GB"
}, },
{
"name": "Comfy-Org/Wan2.2 i2v high noise 14B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for i2v high noise 14B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_i2v_high_noise_14B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp16.safetensors",
"size": "28.6GB"
},
{
"name": "Comfy-Org/Wan2.2 i2v high noise 14B (fp8_scaled)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for i2v high noise 14B (fp8_scaled)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
"size": "14.3GB"
},
{
"name": "Comfy-Org/Wan2.2 i2v low noise 14B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for i2v low noise 14B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_i2v_low_noise_14B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp16.safetensors",
"size": "28.6GB"
},
{
"name": "Comfy-Org/Wan2.2 i2v low noise 14B (fp8_scaled)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for i2v low noise 14B (fp8_scaled)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
"size": "14.3GB"
},
{
"name": "Comfy-Org/Wan2.2 t2v high noise 14B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for t2v high noise 14B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_t2v_high_noise_14B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp16.safetensors",
"size": "28.6GB"
},
{
"name": "Comfy-Org/Wan2.2 t2v high noise 14B (fp8_scaled)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for t2v high noise 14B (fp8_scaled)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
"size": "14.3GB"
},
{
"name": "Comfy-Org/Wan2.2 t2v low noise 14B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for t2v low noise 14B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_t2v_low_noise_14B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp16.safetensors",
"size": "28.6GB"
},
{
"name": "Comfy-Org/Wan2.2 t2v low noise 14B (fp8_scaled)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for t2v low noise 14B (fp8_scaled)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
"size": "14.3GB"
},
{
"name": "Comfy-Org/Wan2.2 ti2v 5B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for ti2v 5B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_ti2v_5B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_ti2v_5B_fp16.safetensors",
"size": "10.0GB"
},
{ {
"name": "Comfy-Org/umt5_xxl_fp16.safetensors", "name": "Comfy-Org/umt5_xxl_fp16.safetensors",
@@ -5246,50 +5033,6 @@
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/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" "size": "15.7GB"
}, },
{
"name": "LTX-Video 2B Distilled v0.9.8",
"type": "checkpoint",
"base": "LTX-Video",
"save_path": "checkpoints/LTXV",
"description": "LTX-Video 2B distilled model v0.9.8 with improved prompt understanding and detail generation.",
"reference": "https://huggingface.co/Lightricks/LTX-Video",
"filename": "ltxv-2b-0.9.8-distilled.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-2b-0.9.8-distilled.safetensors",
"size": "6.34GB"
},
{
"name": "LTX-Video 2B Distilled FP8 v0.9.8",
"type": "checkpoint",
"base": "LTX-Video",
"save_path": "checkpoints/LTXV",
"description": "Quantized LTX-Video 2B distilled model v0.9.8 with improved prompt understanding and detail generation, optimized for lower VRAM usage.",
"reference": "https://huggingface.co/Lightricks/LTX-Video",
"filename": "ltxv-2b-0.9.8-distilled-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-2b-0.9.8-distilled-fp8.safetensors",
"size": "4.46GB"
},
{
"name": "LTX-Video 13B Distilled v0.9.8",
"type": "checkpoint",
"base": "LTX-Video",
"save_path": "checkpoints/LTXV",
"description": "LTX-Video 13B distilled model v0.9.8 with improved prompt understanding and detail generation.",
"reference": "https://huggingface.co/Lightricks/LTX-Video",
"filename": "ltxv-13b-0.9.8-distilled.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.8-distilled.safetensors",
"size": "28.6GB"
},
{
"name": "LTX-Video 13B Distilled FP8 v0.9.8",
"type": "checkpoint",
"base": "LTX-Video",
"save_path": "checkpoints/LTXV",
"description": "Quantized LTX-Video 13B distilled model v0.9.8 with improved prompt understanding and detail generation, optimized for lower VRAM usage.",
"reference": "https://huggingface.co/Lightricks/LTX-Video",
"filename": "ltxv-13b-0.9.8-distilled-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.8-distilled-fp8.safetensors",
"size": "15.7GB"
},
{ {
"name": "LTX-Video 13B Distilled LoRA v0.9.7", "name": "LTX-Video 13B Distilled LoRA v0.9.7",
"type": "lora", "type": "lora",
@@ -5301,50 +5044,6 @@
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/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" "size": "1.33GB"
}, },
{
"name": "LTX-Video ICLoRA Depth 13B v0.9.7",
"type": "lora",
"base": "LTX-Video",
"save_path": "loras",
"description": "In-Context LoRA (IC LoRA) for depth-controlled video-to-video generation with precise depth conditioning.",
"reference": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-depth-13b-0.9.7",
"filename": "ltxv-097-ic-lora-depth-control-comfyui.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-depth-13b-0.9.7/resolve/main/ltxv-097-ic-lora-depth-control-comfyui.safetensors",
"size": "81.9MB"
},
{
"name": "LTX-Video ICLoRA Pose 13B v0.9.7",
"type": "lora",
"base": "LTX-Video",
"save_path": "loras",
"description": "In-Context LoRA (IC LoRA) for pose-controlled video-to-video generation with precise pose conditioning.",
"reference": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-pose-13b-0.9.7",
"filename": "ltxv-097-ic-lora-pose-control-comfyui.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-pose-13b-0.9.7/resolve/main/ltxv-097-ic-lora-pose-control-comfyui.safetensors",
"size": "151MB"
},
{
"name": "LTX-Video ICLoRA Canny 13B v0.9.7",
"type": "lora",
"base": "LTX-Video",
"save_path": "loras",
"description": "In-Context LoRA (IC LoRA) for canny edge-controlled video-to-video generation with precise edge conditioning.",
"reference": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-canny-13b-0.9.7",
"filename": "ltxv-097-ic-lora-canny-control-comfyui.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-canny-13b-0.9.7/resolve/main/ltxv-097-ic-lora-canny-control-comfyui.safetensors",
"size": "81.9MB"
},
{
"name": "LTX-Video ICLoRA Detailer 13B v0.9.8",
"type": "lora",
"base": "LTX-Video",
"save_path": "loras",
"description": "A video detailer model on top of LTXV_13B_098_DEV trained on custom data using In-Context LoRA (IC LoRA) method.",
"reference": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-detailer-13b-0.9.8",
"filename": "ltxv-098-ic-lora-detailer-comfyui.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-detailer-13b-0.9.8/resolve/main/ltxv-098-ic-lora-detailer-comfyui.safetensors",
"size": "1.31GB"
},
{ {
"name": "Latent Bridge Matching for Image Relighting", "name": "Latent Bridge Matching for Image Relighting",
"type": "diffusion_model", "type": "diffusion_model",
@@ -5355,317 +5054,6 @@
"filename": "LBM_relighting.safetensors", "filename": "LBM_relighting.safetensors",
"url": "https://huggingface.co/jasperai/LBM_relighting/resolve/main/model.safetensors", "url": "https://huggingface.co/jasperai/LBM_relighting/resolve/main/model.safetensors",
"size": "5.02GB" "size": "5.02GB"
},
{
"name": "Qwen-Image VAE",
"type": "VAE",
"base": "Qwen-Image",
"save_path": "vae/qwen-image",
"description": "VAE model for Qwen-Image",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
"filename": "qwen_image_vae.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors",
"size": "335MB"
},
{
"name": "Qwen 2.5 VL 7B Text Encoder (fp8_scaled)",
"type": "clip",
"base": "Qwen-2.5-VL",
"save_path": "text_encoders/qwen",
"description": "Qwen 2.5 VL 7B text encoder model (fp8_scaled)",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
"filename": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors",
"size": "3.75GB"
},
{
"name": "Qwen 2.5 VL 7B Text Encoder",
"type": "clip",
"base": "Qwen-2.5-VL",
"save_path": "text_encoders/qwen",
"description": "Qwen 2.5 VL 7B text encoder model",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
"filename": "qwen_2.5_vl_7b.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b.safetensors",
"size": "7.51GB"
},
{
"name": "Qwen-Image Diffusion Model (fp8_e4m3fn)",
"type": "diffusion_model",
"base": "Qwen-Image",
"save_path": "diffusion_models/qwen-image",
"description": "Qwen-Image diffusion model (fp8_e4m3fn)",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
"filename": "qwen_image_fp8_e4m3fn.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_fp8_e4m3fn.safetensors",
"size": "4.89GB"
},
{
"name": "Qwen-Image Diffusion Model (bf16)",
"type": "diffusion_model",
"base": "Qwen-Image",
"save_path": "diffusion_models/qwen-image",
"description": "Qwen-Image diffusion model (bf16)",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
"filename": "qwen_image_bf16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_bf16.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Edit 2509 Diffusion Model (fp8_e4m3fn)",
"type": "diffusion_model",
"base": "Qwen-Image-Edit",
"save_path": "diffusion_models/qwen-image-edit",
"description": "Qwen-Image-Edit 2509 diffusion model (fp8_e4m3fn)",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI",
"filename": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_2509_fp8_e4m3fn.safetensors",
"size": "4.89GB"
},
{
"name": "Qwen-Image-Edit 2509 Diffusion Model (bf16)",
"type": "diffusion_model",
"base": "Qwen-Image-Edit",
"save_path": "diffusion_models/qwen-image-edit",
"description": "Qwen-Image-Edit 2509 diffusion model (bf16)",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI",
"filename": "qwen_image_edit_2509_bf16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_2509_bf16.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Edit Diffusion Model (fp8_e4m3fn)",
"type": "diffusion_model",
"base": "Qwen-Image-Edit",
"save_path": "diffusion_models/qwen-image-edit",
"description": "Qwen-Image-Edit diffusion model (fp8_e4m3fn)",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI",
"filename": "qwen_image_edit_fp8_e4m3fn.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_fp8_e4m3fn.safetensors",
"size": "4.89GB"
},
{
"name": "Qwen-Image-Edit Diffusion Model (bf16)",
"type": "diffusion_model",
"base": "Qwen-Image-Edit",
"save_path": "diffusion_models/qwen-image-edit",
"description": "Qwen-Image-Edit diffusion model (bf16)",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI",
"filename": "qwen_image_edit_bf16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_bf16.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Lightning 8steps V1.0",
"type": "lora",
"base": "Qwen-Image",
"save_path": "loras/qwen-image-lightning",
"description": "Qwen-Image-Lightning 8-step LoRA model V1.0",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Lightning-8steps-V1.0.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V1.0.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Lightning 4steps V1.0",
"type": "lora",
"base": "Qwen-Image",
"save_path": "loras/qwen-image-lightning",
"description": "Qwen-Image-Lightning 4-step LoRA model V1.0",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Lightning-4steps-V1.0.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-4steps-V1.0.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Lightning 4steps V1.0 (bf16)",
"type": "lora",
"base": "Qwen-Image",
"save_path": "loras/qwen-image-lightning",
"description": "Qwen-Image-Lightning 4-step LoRA model V1.0 (bf16)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Lightning-4steps-V1.0-bf16.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-4steps-V1.0-bf16.safetensors",
"size": "19.6GB"
},
{
"name": "Qwen-Image-Lightning 4steps V2.0",
"type": "lora",
"base": "Qwen-Image",
"save_path": "loras/qwen-image-lightning",
"description": "Qwen-Image-Lightning 4-step LoRA model V2.0",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Lightning-4steps-V2.0.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-4steps-V2.0.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Lightning 4steps V2.0 (bf16)",
"type": "lora",
"base": "Qwen-Image",
"save_path": "loras/qwen-image-lightning",
"description": "Qwen-Image-Lightning 4-step LoRA model V2.0 (bf16)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Lightning-4steps-V2.0-bf16.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-4steps-V2.0-bf16.safetensors",
"size": "19.6GB"
},
{
"name": "Qwen-Image-Lightning 8steps V1.1",
"type": "lora",
"base": "Qwen-Image",
"save_path": "loras/qwen-image-lightning",
"description": "Qwen-Image-Lightning 8-step LoRA model V1.1",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Lightning-8steps-V1.1.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V1.1.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Lightning 8steps V1.1 (bf16)",
"type": "lora",
"base": "Qwen-Image",
"save_path": "loras/qwen-image-lightning",
"description": "Qwen-Image-Lightning 8-step LoRA model V1.1 (bf16)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Lightning-8steps-V1.1-bf16.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V1.1-bf16.safetensors",
"size": "19.6GB"
},
{
"name": "Qwen-Image-Lightning 8steps V2.0",
"type": "lora",
"base": "Qwen-Image",
"save_path": "loras/qwen-image-lightning",
"description": "Qwen-Image-Lightning 8-step LoRA model V2.0",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V2.0.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Lightning 8steps V2.0 (bf16)",
"type": "lora",
"base": "Qwen-Image",
"save_path": "loras/qwen-image-lightning",
"description": "Qwen-Image-Lightning 8-step LoRA model V2.0 (bf16)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Lightning-8steps-V2.0-bf16.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V2.0-bf16.safetensors",
"size": "19.6GB"
},
{
"name": "Qwen-Image-Edit-Lightning 4steps V1.0",
"type": "lora",
"base": "Qwen-Image-Edit",
"save_path": "loras/qwen-image-edit-lightning",
"description": "Qwen-Image-Edit-Lightning 4-step LoRA model V1.0",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Edit-Lightning-4steps-V1.0.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-Lightning-4steps-V1.0.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Edit-Lightning 4steps V1.0 (bf16)",
"type": "lora",
"base": "Qwen-Image-Edit",
"save_path": "loras/qwen-image-edit-lightning",
"description": "Qwen-Image-Edit-Lightning 4-step LoRA model V1.0 (bf16)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Edit-Lightning-4steps-V1.0-bf16.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-Lightning-4steps-V1.0-bf16.safetensors",
"size": "19.6GB"
},
{
"name": "Qwen-Image-Edit-Lightning 8steps V1.0",
"type": "lora",
"base": "Qwen-Image-Edit",
"save_path": "loras/qwen-image-edit-lightning",
"description": "Qwen-Image-Edit-Lightning 8-step LoRA model V1.0",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Edit-Lightning-8steps-V1.0.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-Lightning-8steps-V1.0.safetensors",
"size": "9.78GB"
},
{
"name": "Qwen-Image-Edit-Lightning 8steps V1.0 (bf16)",
"type": "lora",
"base": "Qwen-Image-Edit",
"save_path": "loras/qwen-image-edit-lightning",
"description": "Qwen-Image-Edit-Lightning 8-step LoRA model V1.0 (bf16)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Edit-Lightning-8steps-V1.0-bf16.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-Lightning-8steps-V1.0-bf16.safetensors",
"size": "19.6GB"
},
{
"name": "Qwen-Image-Edit-2509-Lightning 4steps V1.0 (bf16)",
"type": "lora",
"base": "Qwen-Image-Edit",
"save_path": "loras/qwen-image-edit-lightning",
"description": "Qwen-Image-Edit-2509-Lightning 4-step LoRA model V1.0 (bf16)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
"size": "19.6GB"
},
{
"name": "Qwen-Image-Edit-2509-Lightning 4steps V1.0 (fp32)",
"type": "lora",
"base": "Qwen-Image-Edit",
"save_path": "loras/qwen-image-edit-lightning",
"description": "Qwen-Image-Edit-2509-Lightning 4-step LoRA model V1.0 (fp32)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Edit-2509-Lightning-4steps-V1.0-fp32.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-fp32.safetensors",
"size": "39.1GB"
},
{
"name": "Qwen-Image-Edit-2509-Lightning 8steps V1.0 (bf16)",
"type": "lora",
"base": "Qwen-Image-Edit",
"save_path": "loras/qwen-image-edit-lightning",
"description": "Qwen-Image-Edit-2509-Lightning 8-step LoRA model V1.0 (bf16)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Edit-2509-Lightning-8steps-V1.0-bf16.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-8steps-V1.0-bf16.safetensors",
"size": "19.6GB"
},
{
"name": "Qwen-Image-Edit-2509-Lightning 8steps V1.0 (fp32)",
"type": "lora",
"base": "Qwen-Image-Edit",
"save_path": "loras/qwen-image-edit-lightning",
"description": "Qwen-Image-Edit-2509-Lightning 8-step LoRA model V1.0 (fp32)",
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
"filename": "Qwen-Image-Edit-2509-Lightning-8steps-V1.0-fp32.safetensors",
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-8steps-V1.0-fp32.safetensors",
"size": "39.1GB"
},
{
"name": "Qwen-Image InstantX ControlNet Union",
"type": "controlnet",
"base": "Qwen-Image",
"save_path": "controlnet/qwen-image/instantx",
"description": "Qwen-Image InstantX ControlNet Union model",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets",
"filename": "Qwen-Image-InstantX-ControlNet-Union.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets/resolve/main/split_files/controlnet/Qwen-Image-InstantX-ControlNet-Union.safetensors",
"size": "2.54GB"
},
{
"name": "Qwen-Image InstantX ControlNet Inpainting",
"type": "controlnet",
"base": "Qwen-Image",
"save_path": "controlnet/qwen-image/instantx",
"description": "Qwen-Image InstantX ControlNet Inpainting model",
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets",
"filename": "Qwen-Image-InstantX-ControlNet-Inpainting.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets/resolve/main/split_files/controlnet/Qwen-Image-InstantX-ControlNet-Inpainting.safetensors",
"size": "2.54GB"
} }
] ]
} }

View File

@@ -1,95 +0,0 @@
# ComfyUI-Manager: Node Database (node_db)
This directory contains the JSON database files that power ComfyUI-Manager's legacy node registry system. While the manager is gradually transitioning to the online Custom Node Registry (CNR), these local JSON files continue to provide important metadata about custom nodes, models, and their integrations.
## Directory Structure
The node_db directory is organized into several subdirectories, each serving a specific purpose:
- **dev/**: Development channel files with latest additions and experimental nodes
- **legacy/**: Historical/legacy nodes that may require special handling
- **new/**: New nodes that have passed initial verification but are still being evaluated
- **forked/**: Forks of existing nodes with modifications
- **tutorial/**: Example and tutorial nodes designed for learning purposes
## Core Database Files
Each subdirectory contains a standard set of JSON files:
- **custom-node-list.json**: Primary database of custom nodes with metadata
- **extension-node-map.json**: Maps between extensions and individual nodes they provide
- **model-list.json**: Catalog of models that can be downloaded through the manager
- **alter-list.json**: Alternative implementations of nodes for compatibility or functionality
- **github-stats.json**: GitHub repository statistics for node popularity metrics
## Database Schema
### custom-node-list.json
```json
{
"custom_nodes": [
{
"title": "Node display name",
"name": "Repository name",
"reference": "Original repository if forked",
"files": ["GitHub URL or other source location"],
"install_type": "git",
"description": "Description of the node's functionality",
"pip": ["optional pip dependencies"],
"js": ["optional JavaScript files"],
"tags": ["categorization tags"]
}
]
}
```
### extension-node-map.json
```json
{
"extension-id": [
["list", "of", "node", "classes"],
{
"author": "Author name",
"description": "Extension description",
"nodename_pattern": "Optional regex pattern for node name matching"
}
]
}
```
## Transition to Custom Node Registry (CNR)
This local database system is being progressively replaced by the online Custom Node Registry (CNR), which provides:
- Real-time updates without manual JSON maintenance
- Improved versioning support
- Better security validation
- Enhanced metadata
The Manager supports both systems simultaneously during the transition period.
## Implementation Details
- The database follows a channel-based architecture for different sources
- Multiple database modes are supported: Channel, Local, and Remote
- The system supports differential updates to minimize bandwidth usage
- Security levels are enforced for different node installations based on source
## Usage in the Application
The Manager's backend uses these database files to:
1. Provide browsable lists of available nodes and models
2. Resolve dependencies for installation
3. Track updates and new versions
4. Map node classes to their source repositories
5. Assess risk levels for installation security
## Maintenance Scripts
Each subdirectory contains a `scan.sh` script that assists with:
- Scanning repositories for new nodes
- Updating metadata
- Validating database integrity
- Generating proper JSON structures
This database system enables a flexible, secure, and comprehensive management system for the ComfyUI ecosystem while the transition to CNR continues.

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
rm ~/.tmp/dev/*.py > /dev/null 2>&1 rm ~/.tmp/dev/*.py > /dev/null 2>&1
python ../../scanner.py ~/.tmp/dev $* python ../../scanner.py ~/.tmp/dev

View File

@@ -1,25 +1,5 @@
{ {
"custom_nodes": [ "custom_nodes": [
{
"author": "synchronicity-labs",
"title": "ComfyUI Sync Lipsync Node",
"reference": "https://github.com/synchronicity-labs/sync-comfyui",
"files": [
"https://github.com/synchronicity-labs/sync-comfyui"
],
"install_type": "git-clone",
"description": "This custom node allows you to perform audio-video lip synchronization inside ComfyUI using a simple interface."
},
{
"author": "joaomede",
"title": "ComfyUI-Unload-Model-Fork",
"reference": "https://github.com/joaomede/ComfyUI-Unload-Model-Fork",
"files": [
"https://github.com/joaomede/ComfyUI-Unload-Model-Fork"
],
"install_type": "git-clone",
"description": "For unloading a model or all models, using the memory management that is already present in ComfyUI. Copied from [a/https://github.com/willblaschko/ComfyUI-Unload-Models](https://github.com/willblaschko/ComfyUI-Unload-Models) but without the unnecessary extra stuff."
},
{ {
"author": "SanDiegoDude", "author": "SanDiegoDude",
"title": "ComfyUI-HiDream-Sampler [WIP]", "title": "ComfyUI-HiDream-Sampler [WIP]",

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,219 +1,5 @@
{ {
"models": [ "models": [
{
"name": "Comfy-Org/Wan2.2 i2v high noise 14B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for i2v high noise 14B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_i2v_high_noise_14B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp16.safetensors",
"size": "28.6GB"
},
{
"name": "Comfy-Org/Wan2.2 i2v high noise 14B (fp8_scaled)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for i2v high noise 14B (fp8_scaled)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
"size": "14.3GB"
},
{
"name": "Comfy-Org/Wan2.2 i2v low noise 14B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for i2v low noise 14B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_i2v_low_noise_14B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp16.safetensors",
"size": "28.6GB"
},
{
"name": "Comfy-Org/Wan2.2 i2v low noise 14B (fp8_scaled)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for i2v low noise 14B (fp8_scaled)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
"size": "14.3GB"
},
{
"name": "Comfy-Org/Wan2.2 t2v high noise 14B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for t2v high noise 14B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_t2v_high_noise_14B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp16.safetensors",
"size": "28.6GB"
},
{
"name": "Comfy-Org/Wan2.2 t2v high noise 14B (fp8_scaled)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for t2v high noise 14B (fp8_scaled)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
"size": "14.3GB"
},
{
"name": "Comfy-Org/Wan2.2 t2v low noise 14B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for t2v low noise 14B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_t2v_low_noise_14B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp16.safetensors",
"size": "28.6GB"
},
{
"name": "Comfy-Org/Wan2.2 t2v low noise 14B (fp8_scaled)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for t2v low noise 14B (fp8_scaled)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
"size": "14.3GB"
},
{
"name": "Comfy-Org/Wan2.2 ti2v 5B (fp16)",
"type": "diffusion_model",
"base": "Wan2.2",
"save_path": "diffusion_models/Wan2.2",
"description": "Wan2.2 diffusion model for ti2v 5B (fp16)",
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
"filename": "wan2.2_ti2v_5B_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_ti2v_5B_fp16.safetensors",
"size": "10.0GB"
},
{
"name": "sam2.1_hiera_tiny.pt",
"type": "sam2.1",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2.1 hiera model (tiny)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2.1_hiera_tiny.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_tiny.pt",
"size": "149.0MB"
},
{
"name": "sam2.1_hiera_small.pt",
"type": "sam2.1",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2.1 hiera model (small)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2.1_hiera_small.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_small.pt",
"size": "176.0MB"
},
{
"name": "sam2.1_hiera_base_plus.pt",
"type": "sam2.1",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2.1 hiera model (base+)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2.1_hiera_base_plus.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_base_plus.pt",
"size": "309.0MB"
},
{
"name": "sam2.1_hiera_large.pt",
"type": "sam2.1",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2.1 hiera model (large)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2.1_hiera_large.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_large.pt",
"size": "857.0MB"
},
{
"name": "sam2_hiera_tiny.pt",
"type": "sam2",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2 hiera model (tiny)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2_hiera_tiny.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_tiny.pt",
"size": "149.0MB"
},
{
"name": "sam2_hiera_small.pt",
"type": "sam2",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2 hiera model (small)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2_hiera_small.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_small.pt",
"size": "176.0MB"
},
{
"name": "sam2_hiera_base_plus.pt",
"type": "sam2",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2 hiera model (base+)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2_hiera_base_plus.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_base_plus.pt",
"size": "309.0MB"
},
{
"name": "sam2_hiera_large.pt",
"type": "sam2",
"base": "SAM",
"save_path": "sams",
"description": "Segmenty Anything SAM 2 hiera model (large)",
"reference": "https://github.com/facebookresearch/sam2#model-description",
"filename": "sam2_hiera_large.pt",
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_large.pt",
"size": "857.0MB"
},
{
"name": "Comfy-Org/omnigen2_fp16.safetensors",
"type": "diffusion_model",
"base": "OmniGen2",
"save_path": "default",
"description": "OmniGen2 diffusion model. This is required for using OmniGen2.",
"reference": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged",
"filename": "omnigen2_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/omnigen2_fp16.safetensors",
"size": "7.93GB"
},
{
"name": "Comfy-Org/qwen_2.5_vl_fp16.safetensors",
"type": "clip",
"base": "qwen-2.5",
"save_path": "default",
"description": "text encoder for OmniGen2",
"reference": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged",
"filename": "qwen_2.5_vl_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged/resolve/main/split_files/text_encoders/qwen_2.5_vl_fp16.safetensors",
"size": "7.51GB"
},
{ {
"name": "Latent Bridge Matching for Image Relighting", "name": "Latent Bridge Matching for Image Relighting",
"type": "diffusion_model", "type": "diffusion_model",
@@ -687,6 +473,224 @@
"filename": "llava_llama3_fp16.safetensors", "filename": "llava_llama3_fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/text_encoders/llava_llama3_fp16.safetensors", "url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/text_encoders/llava_llama3_fp16.safetensors",
"size": "16.1GB" "size": "16.1GB"
},
{
"name": "PixArt-Sigma-XL-2-512-MS.safetensors (diffusion)",
"type": "diffusion_model",
"base": "pixart-sigma",
"save_path": "diffusion_models/PixArt-Sigma",
"description": "PixArt-Sigma Diffusion model",
"reference": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-512-MS",
"filename": "PixArt-Sigma-XL-2-512-MS.safetensors",
"url": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-512-MS/resolve/main/transformer/diffusion_pytorch_model.safetensors",
"size": "2.44GB"
},
{
"name": "PixArt-Sigma-XL-2-1024-MS.safetensors (diffusion)",
"type": "diffusion_model",
"base": "pixart-sigma",
"save_path": "diffusion_models/PixArt-Sigma",
"description": "PixArt-Sigma Diffusion model",
"reference": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS",
"filename": "PixArt-Sigma-XL-2-1024-MS.safetensors",
"url": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS/resolve/main/transformer/diffusion_pytorch_model.safetensors",
"size": "2.44GB"
},
{
"name": "PixArt-XL-2-1024-MS.safetensors (diffusion)",
"type": "diffusion_model",
"base": "pixart-alpha",
"save_path": "diffusion_models/PixArt-Alpha",
"description": "PixArt-Alpha Diffusion model",
"reference": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS",
"filename": "PixArt-XL-2-1024-MS.safetensors",
"url": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS/resolve/main/transformer/diffusion_pytorch_model.safetensors",
"size": "2.45GB"
},
{
"name": "Comfy-Org/hunyuan_video_t2v_720p_bf16.safetensors",
"type": "diffusion_model",
"base": "Hunyuan Video",
"save_path": "diffusion_models/hunyuan_video",
"description": "Huyuan Video diffusion model. repackaged version.",
"reference": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged",
"filename": "hunyuan_video_t2v_720p_bf16.safetensors",
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/diffusion_models/hunyuan_video_t2v_720p_bf16.safetensors",
"size": "25.6GB"
},
{
"name": "Comfy-Org/hunyuan_video_vae_bf16.safetensors",
"type": "VAE",
"base": "Hunyuan Video",
"save_path": "VAE",
"description": "Huyuan Video VAE model. repackaged version.",
"reference": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged",
"filename": "hunyuan_video_vae_bf16.safetensors",
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/vae/hunyuan_video_vae_bf16.safetensors",
"size": "493MB"
},
{
"name": "LTX-Video 2B v0.9.1 Checkpoint",
"type": "checkpoint",
"base": "LTX-Video",
"save_path": "checkpoints/LTXV",
"description": "LTX-Video is the first DiT-based video generation model capable of generating high-quality videos in real-time. It produces 24 FPS videos at a 768x512 resolution faster than they can be watched. Trained on a large-scale dataset of diverse videos, the model generates high-resolution videos with realistic and varied content.",
"reference": "https://huggingface.co/Lightricks/LTX-Video",
"filename": "ltx-video-2b-v0.9.1.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.1.safetensors",
"size": "5.72GB"
},
{
"name": "XLabs-AI/flux-canny-controlnet-v3.safetensors",
"type": "controlnet",
"base": "FLUX.1",
"save_path": "xlabs/controlnets",
"description": "ControlNet checkpoints for FLUX.1-dev model by Black Forest Labs.",
"reference": "https://huggingface.co/XLabs-AI/flux-controlnet-collections",
"filename": "flux-canny-controlnet-v3.safetensors",
"url": "https://huggingface.co/XLabs-AI/flux-controlnet-collections/resolve/main/flux-canny-controlnet-v3.safetensors",
"size": "1.49GB"
},
{
"name": "XLabs-AI/flux-depth-controlnet-v3.safetensors",
"type": "controlnet",
"base": "FLUX.1",
"save_path": "xlabs/controlnets",
"description": "ControlNet checkpoints for FLUX.1-dev model by Black Forest Labs.",
"reference": "https://huggingface.co/XLabs-AI/flux-controlnet-collections",
"filename": "flux-depth-controlnet-v3.safetensors",
"url": "https://huggingface.co/XLabs-AI/flux-controlnet-collections/resolve/main/flux-depth-controlnet-v3.safetensors",
"size": "1.49GB"
},
{
"name": "XLabs-AI/flux-hed-controlnet-v3.safetensors",
"type": "controlnet",
"base": "FLUX.1",
"save_path": "xlabs/controlnets",
"description": "ControlNet checkpoints for FLUX.1-dev model by Black Forest Labs.",
"reference": "https://huggingface.co/XLabs-AI/flux-controlnet-collections",
"filename": "flux-hed-controlnet-v3.safetensors",
"url": "https://huggingface.co/XLabs-AI/flux-controlnet-collections/resolve/main/flux-hed-controlnet-v3.safetensors",
"size": "1.49GB"
},
{
"name": "XLabs-AI/realism_lora.safetensors",
"type": "lora",
"base": "FLUX.1",
"save_path": "xlabs/loras",
"description": "A checkpoint with trained LoRAs for FLUX.1-dev model by Black Forest Labs",
"reference": "https://huggingface.co/XLabs-AI/flux-lora-collection",
"filename": "realism_lora.safetensors",
"url": "https://huggingface.co/XLabs-AI/flux-lora-collection/resolve/main/realism_lora.safetensors",
"size": "44.8MB"
},
{
"name": "XLabs-AI/art_lora.safetensors",
"type": "lora",
"base": "FLUX.1",
"save_path": "xlabs/loras",
"description": "A checkpoint with trained LoRAs for FLUX.1-dev model by Black Forest Labs",
"reference": "https://huggingface.co/XLabs-AI/flux-lora-collection",
"filename": "art_lora.safetensors",
"url": "https://huggingface.co/XLabs-AI/flux-lora-collection/resolve/main/scenery_lora.safetensors",
"size": "44.8MB"
},
{
"name": "XLabs-AI/mjv6_lora.safetensors",
"type": "lora",
"base": "FLUX.1",
"save_path": "xlabs/loras",
"description": "A checkpoint with trained LoRAs for FLUX.1-dev model by Black Forest Labs",
"reference": "https://huggingface.co/XLabs-AI/flux-lora-collection",
"filename": "mjv6_lora.safetensors",
"url": "https://huggingface.co/XLabs-AI/flux-lora-collection/resolve/main/mjv6_lora.safetensors",
"size": "44.8MB"
},
{
"name": "XLabs-AI/flux-ip-adapter",
"type": "lora",
"base": "FLUX.1",
"save_path": "xlabs/ipadapters",
"description": "A checkpoint with trained LoRAs for FLUX.1-dev model by Black Forest Labs",
"reference": "https://huggingface.co/XLabs-AI/flux-ip-adapter",
"filename": "ip_adapter.safetensors",
"url": "https://huggingface.co/XLabs-AI/flux-ip-adapter/resolve/main/ip_adapter.safetensors",
"size": "982MB"
},
{
"name": "stabilityai/SD3.5-Large-Controlnet-Blur",
"type": "controlnet",
"base": "SD3.5",
"save_path": "controlnet/SD3.5",
"description": "Blur Controlnet model for SD3.5 Large",
"reference": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets",
"filename": "sd3.5_large_controlnet_blur.safetensors",
"url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets/resolve/main/sd3.5_large_controlnet_blur.safetensors",
"size": "8.65GB"
},
{
"name": "stabilityai/SD3.5-Large-Controlnet-Canny",
"type": "controlnet",
"base": "SD3.5",
"save_path": "controlnet/SD3.5",
"description": "Canny Controlnet model for SD3.5 Large",
"reference": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets",
"filename": "sd3.5_large_controlnet_canny.safetensors",
"url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets/resolve/main/sd3.5_large_controlnet_canny.safetensors",
"size": "8.65GB"
},
{
"name": "stabilityai/SD3.5-Large-Controlnet-Depth",
"type": "controlnet",
"base": "SD3.5",
"save_path": "controlnet/SD3.5",
"description": "Depth Controlnet model for SD3.5 Large",
"reference": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets",
"filename": "sd3.5_large_controlnet_depth.safetensors",
"url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets/resolve/main/sd3.5_large_controlnet_depth.safetensors",
"size": "8.65GB"
},
{
"name": "LTX-Video 2B v0.9 Checkpoint",
"type": "checkpoint",
"base": "LTX-Video",
"save_path": "checkpoints/LTXV",
"description": "LTX-Video is the first DiT-based video generation model capable of generating high-quality videos in real-time. It produces 24 FPS videos at a 768x512 resolution faster than they can be watched. Trained on a large-scale dataset of diverse videos, the model generates high-resolution videos with realistic and varied content.",
"reference": "https://huggingface.co/Lightricks/LTX-Video",
"filename": "ltx-video-2b-v0.9.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.safetensors",
"size": "9.37GB"
},
{
"name": "InstantX/FLUX.1-dev-IP-Adapter",
"type": "IP-Adapter",
"base": "FLUX.1",
"save_path": "ipadapter-flux",
"description": "FLUX.1-dev-IP-Adapter",
"reference": "https://huggingface.co/InstantX/FLUX.1-dev-IP-Adapter",
"filename": "ip-adapter.bin",
"url": "https://huggingface.co/InstantX/FLUX.1-dev-IP-Adapter/resolve/main/ip-adapter.bin",
"size": "5.29GB"
},
{
"name": "Comfy-Org/sigclip_vision_384 (patch14_384)",
"type": "clip_vision",
"base": "sigclip",
"save_path": "clip_vision",
"description": "This clip vision model is required for FLUX.1 Redux.",
"reference": "https://huggingface.co/Comfy-Org/sigclip_vision_384/tree/main",
"filename": "sigclip_vision_patch14_384.safetensors",
"url": "https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors",
"size": "857MB"
} }
] ]
} }

View File

@@ -10,16 +10,6 @@
"install_type": "git-clone", "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." "description": "A minimal template for creating React/TypeScript frontend extensions for ComfyUI, with complete boilerplate setup including internationalization and unit testing."
}, },
{
"author": "comfyui-wiki",
"title": "ComfyUI-i18n-demo",
"reference": "https://github.com/comfyui-wiki/ComfyUI-i18n-demo",
"files": [
"https://github.com/comfyui-wiki/ComfyUI-i18n-demo"
],
"install_type": "git-clone",
"description": "ComfyUI custom node develop i18n support demo "
},
{ {
"author": "Suzie1", "author": "Suzie1",
"title": "Guide To Making Custom Nodes in ComfyUI", "title": "Guide To Making Custom Nodes in ComfyUI",
@@ -341,36 +331,6 @@
], ],
"description": "Dynamic Node examples for ComfyUI", "description": "Dynamic Node examples for ComfyUI",
"install_type": "git-clone" "install_type": "git-clone"
},
{
"author": "Jonathon-Doran",
"title": "remote-combo-demo",
"reference": "https://github.com/Jonathon-Doran/remote-combo-demo",
"files": [
"https://github.com/Jonathon-Doran/remote-combo-demo"
],
"install_type": "git-clone",
"description": "A minimal test suite demonstrating how remote COMBO inputs behave in ComfyUI, with and without force_input"
},
{
"author": "J1mB091",
"title": "ComfyUI-J1mB091 Custom Nodes",
"reference": "https://github.com/J1mB091/ComfyUI-J1mB091",
"files": [
"https://github.com/J1mB091/ComfyUI-J1mB091"
],
"install_type": "git-clone",
"description": "Vibe Coded ComfyUI Custom Nodes"
},
{
"author": "aiforhumans",
"title": "XDev Nodes - Complete Toolkit",
"reference": "https://github.com/aiforhumans/comfyui-xdev-nodes",
"files": [
"https://github.com/aiforhumans/comfyui-xdev-nodes"
],
"install_type": "git-clone",
"description": "Complete ComfyUI development toolkit with 8 professional nodes including VAE tools, universal type testing, and comprehensive debugging infrastructure."
} }
] ]
} }

View File

@@ -1,903 +0,0 @@
openapi: 3.1.0
info:
title: ComfyUI-Manager API
description: |
API for ComfyUI-Manager, a comprehensive management tool for ComfyUI custom nodes, models, and components.
This API enables programmatic access to node management, model downloading, snapshot operations,
and overall system configuration.
version: "3.32.3"
contact:
name: ComfyUI-Manager Maintainers
servers:
- url: '/'
description: Default ComfyUI server
# Common API components
components:
schemas:
Error:
type: object
properties:
error:
type: string
description: Error message
NodePackageMetadata:
type: object
properties:
title:
type: string
description: Display name of the node package
name:
type: string
description: Repository/package name
files:
type: array
items:
type: string
description: Source URLs for the package
description:
type: string
description: Description of the node package functionality
install_type:
type: string
enum: [git, copy, pip]
description: Installation method
version:
type: string
description: Version identifier
id:
type: string
description: Unique identifier for the node package
ui_id:
type: string
description: ID for UI reference
channel:
type: string
description: Source channel
mode:
type: string
description: Source mode
ModelMetadata:
type: object
properties:
name:
type: string
description: Name of the model
type:
type: string
description: Type of model
base:
type: string
description: Base model type
save_path:
type: string
description: Path for saving the model
url:
type: string
description: Download URL
filename:
type: string
description: Target filename
ui_id:
type: string
description: ID for UI reference
SnapshotItem:
type: string
description: Name of the snapshot
QueueStatus:
type: object
properties:
total_count:
type: integer
description: Total number of tasks
done_count:
type: integer
description: Number of completed tasks
in_progress_count:
type: integer
description: Number of tasks in progress
is_processing:
type: boolean
description: Whether the queue is currently processing
ImportFailInfoBulkRequest:
type: object
properties:
cnr_ids:
type: array
items:
type: string
description: A list of CNR IDs to check.
urls:
type: array
items:
type: string
description: A list of repository URLs to check.
ImportFailInfoBulkResponse:
type: object
additionalProperties:
$ref: '#/components/schemas/ImportFailInfoItem'
description: >-
A dictionary where each key is a cnr_id or url from the request,
and the value is the corresponding error info.
ImportFailInfoItem:
oneOf:
- type: object
properties:
error:
type: string
traceback:
type: string
- type: "null"
securitySchemes:
securityLevel:
type: apiKey
in: header
name: Security-Level
description: Security level for sensitive operations
parameters:
modeParam:
name: mode
in: query
description: Source mode (e.g., "local", "remote")
schema:
type: string
enum: [local, remote, default]
targetParam:
name: target
in: query
description: Target identifier
required: true
schema:
type: string
valueParam:
name: value
in: query
description: New value to set
required: true
schema:
type: string
# API Paths
paths:
# Custom Nodes Endpoints
/customnode/getmappings:
get:
summary: Get node-to-package mappings
description: Provides unified mapping between nodes and node packages
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: array
items:
type: array
description: Mapping of node packages to node classes
/customnode/fetch_updates:
get:
summary: Check for updates
description: Fetches updates for custom nodes
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: No updates available
'201':
description: Updates found
'400':
description: Error occurred
/customnode/installed:
get:
summary: Get installed custom nodes
description: Returns a list of installed node packages
parameters:
- name: mode
in: query
description: Lists mode, default or imported
schema:
type: string
enum: [default, imported]
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
additionalProperties:
$ref: '#/components/schemas/NodePackageMetadata'
/customnode/getlist:
get:
summary: Get custom node list
description: Provides a list of available custom nodes
parameters:
- $ref: '#/components/parameters/modeParam'
- name: skip_update
in: query
description: Skip update check
schema:
type: boolean
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
channel:
type: string
node_packs:
type: object
additionalProperties:
$ref: '#/components/schemas/NodePackageMetadata'
/customnode/alternatives:
get:
summary: Get alternative node options
description: Provides alternatives for nodes
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: object
/customnode/versions/{node_name}:
get:
summary: Get available versions for a node
description: Lists all available versions for a specific node
parameters:
- name: node_name
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: array
items:
type: object
properties:
version:
type: string
'400':
description: Node not found
/customnode/disabled_versions/{node_name}:
get:
summary: Get disabled versions for a node
description: Lists all disabled versions for a specific node
parameters:
- name: node_name
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: array
items:
type: object
properties:
version:
type: string
'400':
description: Node not found
/customnode/import_fail_info:
post:
summary: Get import failure information
description: Returns information about why a node failed to import
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
cnr_id:
type: string
url:
type: string
responses:
'200':
description: Successful operation
'400':
description: No information available
/v2/customnode/import_fail_info_bulk:
post:
summary: Get import failure info for multiple nodes
description: Retrieves recorded import failure information for a list of custom nodes.
tags:
- customnode
requestBody:
description: A list of CNR IDs or repository URLs to check.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ImportFailInfoBulkRequest'
responses:
'200':
description: A dictionary containing the import failure information.
content:
application/json:
schema:
$ref: '#/components/schemas/ImportFailInfoBulkResponse'
'400':
description: Bad Request. The request body is invalid.
'500':
description: Internal Server Error.
/customnode/install/git_url:
post:
summary: Install custom node via Git URL
description: Installs a custom node from a Git repository URL
security:
- securityLevel: []
requestBody:
required: true
content:
text/plain:
schema:
type: string
responses:
'200':
description: Installation successful or already installed
'400':
description: Installation failed
'403':
description: Security policy violation
/customnode/install/pip:
post:
summary: Install custom node dependencies via pip
description: Installs Python package dependencies for custom nodes
security:
- securityLevel: []
requestBody:
required: true
content:
text/plain:
schema:
type: string
responses:
'200':
description: Installation successful
'403':
description: Security policy violation
# Model Management Endpoints
/externalmodel/getlist:
get:
summary: Get external model list
description: Provides a list of available external models
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
models:
type: array
items:
$ref: '#/components/schemas/ModelMetadata'
# Queue Management Endpoints
/manager/queue/update_all:
get:
summary: Update all custom nodes
description: Queues update operations for all installed custom nodes
security:
- securityLevel: []
parameters:
- $ref: '#/components/parameters/modeParam'
responses:
'200':
description: Update queued successfully
'401':
description: Processing already in progress
'403':
description: Security policy violation
/manager/queue/reset:
get:
summary: Reset queue
description: Resets the operation queue
responses:
'200':
description: Queue reset successfully
/manager/queue/status:
get:
summary: Get queue status
description: Returns the current status of the operation queue
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/QueueStatus'
/manager/queue/install:
post:
summary: Install custom node
description: Queues installation of a custom node
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Installation queued successfully
'403':
description: Security policy violation
'404':
description: Target node not found or security issue
/manager/queue/start:
get:
summary: Start queue processing
description: Starts processing the operation queue
responses:
'200':
description: Processing started
'201':
description: Processing already in progress
/manager/queue/fix:
post:
summary: Fix custom node
description: Attempts to fix a broken custom node installation
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Fix operation queued successfully
'403':
description: Security policy violation
/manager/queue/reinstall:
post:
summary: Reinstall custom node
description: Uninstalls and then reinstalls a custom node
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Reinstall operation queued successfully
'403':
description: Security policy violation
/manager/queue/uninstall:
post:
summary: Uninstall custom node
description: Queues uninstallation of a custom node
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Uninstallation queued successfully
'403':
description: Security policy violation
/manager/queue/update:
post:
summary: Update custom node
description: Queues update of a custom node
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Update queued successfully
'403':
description: Security policy violation
/manager/queue/disable:
post:
summary: Disable custom node
description: Disables a custom node without uninstalling it
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodePackageMetadata'
responses:
'200':
description: Disable operation queued successfully
/manager/queue/update_comfyui:
get:
summary: Update ComfyUI
description: Queues an update operation for ComfyUI itself
responses:
'200':
description: Update queued successfully
/manager/queue/install_model:
post:
summary: Install model
description: Queues installation of a model
security:
- securityLevel: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ModelMetadata'
responses:
'200':
description: Installation queued successfully
'400':
description: Invalid model request
'403':
description: Security policy violation
# Snapshot Management Endpoints
/snapshot/getlist:
get:
summary: Get snapshot list
description: Returns a list of available snapshots
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/SnapshotItem'
/snapshot/remove:
get:
summary: Remove snapshot
description: Removes a specified snapshot
security:
- securityLevel: []
parameters:
- $ref: '#/components/parameters/targetParam'
responses:
'200':
description: Snapshot removed successfully
'400':
description: Error removing snapshot
'403':
description: Security policy violation
/snapshot/restore:
get:
summary: Restore snapshot
description: Restores a specified snapshot
security:
- securityLevel: []
parameters:
- $ref: '#/components/parameters/targetParam'
responses:
'200':
description: Snapshot restoration scheduled
'400':
description: Error restoring snapshot
'403':
description: Security policy violation
/snapshot/get_current:
get:
summary: Get current snapshot
description: Returns the current system state as a snapshot
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
'400':
description: Error creating snapshot
/snapshot/save:
get:
summary: Save snapshot
description: Saves the current system state as a new snapshot
responses:
'200':
description: Snapshot saved successfully
'400':
description: Error saving snapshot
# ComfyUI Management Endpoints
/comfyui_manager/comfyui_versions:
get:
summary: Get ComfyUI versions
description: Returns available and current ComfyUI versions
responses:
'200':
description: Successful operation
content:
application/json:
schema:
type: object
properties:
versions:
type: array
items:
type: string
current:
type: string
'400':
description: Error retrieving versions
/comfyui_manager/comfyui_switch_version:
get:
summary: Switch ComfyUI version
description: Switches to a specified ComfyUI version
parameters:
- name: ver
in: query
description: Target version
schema:
type: string
responses:
'200':
description: Version switch successful
'400':
description: Error switching version
/manager/reboot:
get:
summary: Reboot ComfyUI
description: Restarts the ComfyUI server
security:
- securityLevel: []
responses:
'200':
description: Reboot initiated
'403':
description: Security policy violation
# Configuration Endpoints
/manager/preview_method:
get:
summary: Get or set preview method
description: Gets or sets the latent preview method
parameters:
- name: value
in: query
required: false
description: New preview method
schema:
type: string
enum: [auto, latent2rgb, taesd, none]
responses:
'200':
description: Setting updated or current value returned
content:
text/plain:
schema:
type: string
/manager/db_mode:
get:
summary: Get or set database mode
description: Gets or sets the database mode
parameters:
- name: value
in: query
required: false
description: New database mode
schema:
type: string
enum: [channel, local, remote]
responses:
'200':
description: Setting updated or current value returned
content:
text/plain:
schema:
type: string
/manager/policy/component:
get:
summary: Get or set component policy
description: Gets or sets the component policy
parameters:
- name: value
in: query
required: false
description: New component policy
schema:
type: string
responses:
'200':
description: Setting updated or current value returned
content:
text/plain:
schema:
type: string
/manager/policy/update:
get:
summary: Get or set update policy
description: Gets or sets the update policy
parameters:
- name: value
in: query
required: false
description: New update policy
schema:
type: string
enum: [stable, nightly, nightly-comfyui]
responses:
'200':
description: Setting updated or current value returned
content:
text/plain:
schema:
type: string
/manager/channel_url_list:
get:
summary: Get or set channel URL
description: Gets or sets the channel URL for custom node sources
parameters:
- name: value
in: query
required: false
description: New channel name
schema:
type: string
responses:
'200':
description: Setting updated or channel list returned
content:
application/json:
schema:
type: object
properties:
selected:
type: string
list:
type: array
items:
type: object
properties:
name:
type: string
url:
type: string
# Component Management Endpoints
/manager/component/save:
post:
summary: Save component
description: Saves a reusable workflow component
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
workflow:
type: object
responses:
'200':
description: Component saved successfully
content:
text/plain:
schema:
type: string
'400':
description: Error saving component
/manager/component/loads:
post:
summary: Load components
description: Loads all available workflow components
responses:
'200':
description: Components loaded successfully
content:
application/json:
schema:
type: object
'400':
description: Error loading components
# Miscellaneous Endpoints
/manager/version:
get:
summary: Get manager version
description: Returns the current version of ComfyUI-Manager
responses:
'200':
description: Successful operation
content:
text/plain:
schema:
type: string
/manager/notice:
get:
summary: Get manager notice
description: Returns HTML content with notices and version information
responses:
'200':
description: Successful operation
content:
text/html:
schema:
type: string

View File

@@ -38,6 +38,7 @@ else:
def current_timestamp(): def current_timestamp():
return str(time.time()).split('.')[0] return str(time.time()).split('.')[0]
security_check.security_check()
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'} cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia'] cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
@@ -118,15 +119,20 @@ def check_file_logging():
read_config() read_config()
read_uv_mode() read_uv_mode()
security_check.security_check()
check_file_logging() check_file_logging()
cm_global.pip_overrides = {} 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): if os.path.exists(manager_pip_overrides_path):
with open(manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file: 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 = json.load(json_file)
if sys.version_info < (3, 13):
cm_global.pip_overrides['numpy'] = 'numpy<2'
if os.path.exists(manager_pip_blacklist_path): if os.path.exists(manager_pip_blacklist_path):
with open(manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f: with open(manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
@@ -338,12 +344,7 @@ try:
log_file.write(message) log_file.write(message)
else: else:
log_file.write(f"[{timestamp}] {message}") log_file.write(f"[{timestamp}] {message}")
log_file.flush()
try:
log_file.flush()
except Exception:
pass
self.last_char = message if message == '' else message[-1] self.last_char = message if message == '' else message[-1]
if not file_only: if not file_only:
@@ -356,10 +357,7 @@ try:
original_stderr.flush() original_stderr.flush()
def flush(self): def flush(self):
try: log_file.flush()
log_file.flush()
except Exception:
pass
with std_log_lock: with std_log_lock:
if self.is_stdout: if self.is_stdout:

View File

@@ -1,9 +1,9 @@
[project] [project]
name = "comfyui-manager" name = "comfyui-manager"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI." description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
version = "3.37" version = "3.32.3"
license = { file = "LICENSE.txt" } license = { file = "LICENSE.txt" }
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"] dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]
[project.urls] [project.urls]
Repository = "https://github.com/ltdrdata/ComfyUI-Manager" Repository = "https://github.com/ltdrdata/ComfyUI-Manager"

View File

@@ -1,8 +1,8 @@
GitPython GitPython
PyGithub PyGithub
matrix-nio matrix-client==0.4.0
transformers transformers
huggingface-hub huggingface-hub>0.20
typer typer
rich rich
typing-extensions typing-extensions

View File

@@ -13,7 +13,7 @@ builtin_nodes = set()
import sys import sys
from urllib.parse import urlparse from urllib.parse import urlparse
from github import Github, Auth from github import Github
def download_url(url, dest_folder, filename=None): def download_url(url, dest_folder, filename=None):
@@ -53,8 +53,7 @@ skip_update = '--skip-update' in sys.argv or '--skip-all' in sys.argv
skip_stat_update = '--skip-stat-update' in sys.argv or '--skip-all' in sys.argv skip_stat_update = '--skip-stat-update' in sys.argv or '--skip-all' in sys.argv
if not skip_stat_update: if not skip_stat_update:
auth = Auth.Token(os.environ.get('GITHUB_TOKEN')) g = Github(os.environ.get('GITHUB_TOKEN'))
g = Github(auth=auth)
else: else:
g = None g = None
@@ -103,8 +102,12 @@ def extract_nodes(code_text):
def scan_in_file(filename, is_builtin=False): def scan_in_file(filename, is_builtin=False):
global builtin_nodes global builtin_nodes
with open(filename, encoding='utf-8', errors='ignore') as file: try:
code = file.read() with open(filename, encoding='utf-8') as file:
code = file.read()
except UnicodeDecodeError:
with open(filename, encoding='cp949') as file:
code = file.read()
pattern = r"_CLASS_MAPPINGS\s*=\s*{([^}]*)}" pattern = r"_CLASS_MAPPINGS\s*=\s*{([^}]*)}"
regex = re.compile(pattern, re.MULTILINE | re.DOTALL) regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
@@ -256,13 +259,13 @@ def clone_or_pull_git_repository(git_url):
repo.git.submodule('update', '--init', '--recursive') repo.git.submodule('update', '--init', '--recursive')
print(f"Pulling {repo_name}...") print(f"Pulling {repo_name}...")
except Exception as e: except Exception as e:
print(f"Failed to pull '{repo_name}': {e}") print(f"Pulling {repo_name} failed: {e}")
else: else:
try: try:
Repo.clone_from(git_url, repo_dir, recursive=True) Repo.clone_from(git_url, repo_dir, recursive=True)
print(f"Cloning {repo_name}...") print(f"Cloning {repo_name}...")
except Exception as e: except Exception as e:
print(f"Failed to clone '{repo_name}': {e}") print(f"Cloning {repo_name} failed: {e}")
def update_custom_nodes(): def update_custom_nodes():
@@ -294,7 +297,7 @@ def update_custom_nodes():
pass pass
def is_rate_limit_exceeded(): def is_rate_limit_exceeded():
return g.rate_limiting[0] <= 20 return g.rate_limiting[0] == 0
if is_rate_limit_exceeded(): if is_rate_limit_exceeded():
print(f"GitHub API Rate Limit Exceeded: remained - {(g.rate_limiting_resettime - datetime.datetime.now().timestamp())/60:.2f} min") print(f"GitHub API Rate Limit Exceeded: remained - {(g.rate_limiting_resettime - datetime.datetime.now().timestamp())/60:.2f} min")
@@ -497,15 +500,8 @@ def gen_json(node_info):
nodes_in_url, metadata_in_url = data[git_url] nodes_in_url, metadata_in_url = data[git_url]
nodes = set(nodes_in_url) nodes = set(nodes_in_url)
try: for x, desc in node_list_json.items():
for x, desc in node_list_json.items(): nodes.add(x.strip())
nodes.add(x.strip())
except Exception as e:
print(f"\nERROR: Invalid json format '{node_list_json_path}'")
print("------------------------------------------------------")
print(e)
print("------------------------------------------------------")
node_list_json = {}
metadata_in_url['title_aux'] = title metadata_in_url['title_aux'] = title

19
tests-api/.gitignore vendored Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
# Make tests-api directory a proper package

237
tests-api/conftest.py Normal file
View 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

View File

@@ -0,0 +1 @@
# Make tests-api/mocks directory a proper package

View 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
View 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))

View 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
View 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
View 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"

View 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

View 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"

View 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
View 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}")

View 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
View 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

View 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"]

View 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}"

View File

@@ -0,0 +1 @@
# Make utils directory a proper package

View 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)
]

View 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
)