Compare commits
431 Commits
3.7.2
...
tests/api-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da87651e53 | ||
|
|
416122d61d | ||
|
|
d3c625e791 | ||
|
|
ca2c41783c | ||
|
|
e2a6446585 | ||
|
|
839790b5ab | ||
|
|
58b9946936 | ||
|
|
a19ba22eaf | ||
|
|
117715aa22 | ||
|
|
891a5a85ee | ||
|
|
166debfabb | ||
|
|
7258a09fe5 | ||
|
|
058a436187 | ||
|
|
1950802c55 | ||
|
|
eb52a03372 | ||
|
|
f8aa428be3 | ||
|
|
ec0893f136 | ||
|
|
92b99ea963 | ||
|
|
02cd52bb65 | ||
|
|
af1ec2c87b | ||
|
|
41006c3a33 | ||
|
|
116a6d500d | ||
|
|
87d0ac807f | ||
|
|
fc943172eb | ||
|
|
9daa5a2fbd | ||
|
|
b7b2746a61 | ||
|
|
d66a4fbfc8 | ||
|
|
683a172ad8 | ||
|
|
6e12358f5a | ||
|
|
8bcf16dc90 | ||
|
|
65c0a2a1f5 | ||
|
|
115236eb9c | ||
|
|
08de942abe | ||
|
|
e9dff83290 | ||
|
|
3bc6c7584d | ||
|
|
22a2bf1584 | ||
|
|
79ece5f72c | ||
|
|
5da6fe1373 | ||
|
|
48c10d0b95 | ||
|
|
9bb56b1457 | ||
|
|
83420fd828 | ||
|
|
52f4b9506f | ||
|
|
b501e9b20b | ||
|
|
1f7ae5319a | ||
|
|
68c201239d | ||
|
|
6e4e43f612 | ||
|
|
81c3708f39 | ||
|
|
f4d2bbde34 | ||
|
|
d14b42a42c | ||
|
|
0e9c32344c | ||
|
|
30c4ea06af | ||
|
|
8211264993 | ||
|
|
67cf5b49e1 | ||
|
|
8e7ba18e05 | ||
|
|
8359e1063e | ||
|
|
ca078e54b9 | ||
|
|
f7e930c5a2 | ||
|
|
479d95e1c8 | ||
|
|
2b0ff08eef | ||
|
|
67a487db15 | ||
|
|
2488cb3458 | ||
|
|
157e6336fa | ||
|
|
d808a1f406 | ||
|
|
2bb4d8cd63 | ||
|
|
a8164e1631 | ||
|
|
a31d286945 | ||
|
|
12eeef4cf0 | ||
|
|
ce8e6dc36e | ||
|
|
7a32e544a7 | ||
|
|
e16e9d7a0e | ||
|
|
821f908dbc | ||
|
|
e007e6f897 | ||
|
|
94f496fd65 | ||
|
|
d2ce35d2e6 | ||
|
|
2eeebb32dc | ||
|
|
f6d636d82f | ||
|
|
0cd397623e | ||
|
|
5978b6c9ee | ||
|
|
9e132811bc | ||
|
|
3a3b5c1f92 | ||
|
|
26be01ff82 | ||
|
|
8f6dd92374 | ||
|
|
d50b71a887 | ||
|
|
3bc9cbc767 | ||
|
|
b6f6b4fd8a | ||
|
|
a66bada8a3 | ||
|
|
a804f7de19 | ||
|
|
72a61a9966 | ||
|
|
b08bb658ea | ||
|
|
7b28bf608b | ||
|
|
b57747fdf1 | ||
|
|
0735271b10 | ||
|
|
770cd0f9f5 | ||
|
|
32b6266dd9 | ||
|
|
2a8412a2bf | ||
|
|
0c4d289002 | ||
|
|
cee01fec25 | ||
|
|
f00686f3f2 | ||
|
|
bd33f7726e | ||
|
|
22ab526b0c | ||
|
|
af269d198d | ||
|
|
995ef6356e | ||
|
|
aa3bf77c28 | ||
|
|
15667c1259 | ||
|
|
c7b6b565da | ||
|
|
3214ab52c6 | ||
|
|
e3062ff613 | ||
|
|
036b63efe7 | ||
|
|
8d3e1d60d0 | ||
|
|
59876452f4 | ||
|
|
04972ad87f | ||
|
|
c7e69f4e26 | ||
|
|
7a59b6d0d9 | ||
|
|
d227ad97a4 | ||
|
|
b93a474dae | ||
|
|
a5fe075bf3 | ||
|
|
17e5c3d2f5 | ||
|
|
27bfc539f7 | ||
|
|
821fded09d | ||
|
|
ec4a2aa873 | ||
|
|
d6b2d54f3f | ||
|
|
97ae67bb9a | ||
|
|
765514a33f | ||
|
|
e2cdcc96c4 | ||
|
|
0738b2a73f | ||
|
|
98db79910e | ||
|
|
0b21a05aac | ||
|
|
4b71db54aa | ||
|
|
a6bc890f36 | ||
|
|
76903c39e1 | ||
|
|
cf9ed1c631 | ||
|
|
50fc1389b0 | ||
|
|
c70cb2868b | ||
|
|
12fa571aa2 | ||
|
|
4a3018760f | ||
|
|
d005d06cf8 | ||
|
|
a87e3f9ee9 | ||
|
|
52b9a3f3a0 | ||
|
|
c01a7e41d0 | ||
|
|
fe301bb91a | ||
|
|
a42953e3be | ||
|
|
1899255a69 | ||
|
|
908a1009d2 | ||
|
|
fb9c68fc32 | ||
|
|
d54ec0eb05 | ||
|
|
a386948fd1 | ||
|
|
007b812ede | ||
|
|
0ddb0cec03 | ||
|
|
e687f83fbf | ||
|
|
458c9de70f | ||
|
|
87a652d038 | ||
|
|
d889df4c89 | ||
|
|
a2e72d26aa | ||
|
|
a4fdc874e7 | ||
|
|
dfbe382d60 | ||
|
|
0d56ebb1bf | ||
|
|
9e66da174e | ||
|
|
55fcb00168 | ||
|
|
68aa534e1d | ||
|
|
7fd94a401b | ||
|
|
2b9cec50ce | ||
|
|
d1a80cf082 | ||
|
|
fb445aa510 | ||
|
|
4b904934ef | ||
|
|
d6295a00e6 | ||
|
|
3b01673829 | ||
|
|
a5e83a807f | ||
|
|
ddd766ce58 | ||
|
|
a6d2fd36fb | ||
|
|
9156d6bdba | ||
|
|
d18a3ffeff | ||
|
|
e933eaa2b0 | ||
|
|
5393653ddc | ||
|
|
1f3274d3f5 | ||
|
|
39eaa76b8a | ||
|
|
e5396713ce | ||
|
|
79943de808 | ||
|
|
e05f329602 | ||
|
|
eed0e8ebea | ||
|
|
731eb4fcbe | ||
|
|
44a63e4b6d | ||
|
|
7651e5e48b | ||
|
|
2449636d32 | ||
|
|
f9990ca8eb | ||
|
|
c3eed981c0 | ||
|
|
bbb54d4a08 | ||
|
|
4566c585db | ||
|
|
a946338a18 | ||
|
|
0a60a44478 | ||
|
|
cef0ad6707 | ||
|
|
7176f0837a | ||
|
|
6b1f2b2d9d | ||
|
|
38a1a9b320 | ||
|
|
402e2c384f | ||
|
|
9d5faa096c | ||
|
|
97d0dc20f1 | ||
|
|
8d99ff07b6 | ||
|
|
04fa540a8c | ||
|
|
eb41867e04 | ||
|
|
eee5d7d9e8 | ||
|
|
e983f9ed35 | ||
|
|
8b16ef641b | ||
|
|
e87d616b7a | ||
|
|
2220f325fc | ||
|
|
b53ed47ccb | ||
|
|
39df2743fe | ||
|
|
3f729aaf03 | ||
|
|
b7324621e4 | ||
|
|
e8c782c8e1 | ||
|
|
9136505565 | ||
|
|
f406d728cc | ||
|
|
d649ca47c6 | ||
|
|
e8111527b4 | ||
|
|
2af66d7efc | ||
|
|
27706f37f6 | ||
|
|
3de17b2fa6 | ||
|
|
22ecb5de95 | ||
|
|
992b8b3cb5 | ||
|
|
bebc16d5a6 | ||
|
|
ddb719f866 | ||
|
|
0bd1bf2605 | ||
|
|
fd32ba4035 | ||
|
|
22f723b920 | ||
|
|
1248bd0413 | ||
|
|
c150eec2b6 | ||
|
|
c7248c2d47 | ||
|
|
e71e68e298 | ||
|
|
6969557693 | ||
|
|
f6be5ad839 | ||
|
|
cebe3664fd | ||
|
|
cdab465c90 | ||
|
|
144384655c | ||
|
|
0e213d6dab | ||
|
|
21294a4e4a | ||
|
|
3ba4d44d9e | ||
|
|
1f86ef5a37 | ||
|
|
fac60da333 | ||
|
|
5a5a37dfee | ||
|
|
0d487bc14f | ||
|
|
a52b4eb5ed | ||
|
|
f1b7f5f52f | ||
|
|
5ef58652bf | ||
|
|
e26a9e75c6 | ||
|
|
b0035ff4a7 | ||
|
|
94b6f9b2fe | ||
|
|
cad1482b3f | ||
|
|
ea7aafb3e6 | ||
|
|
42b15ad4a5 | ||
|
|
d3d613cca9 | ||
|
|
86893d999a | ||
|
|
4fd17b0bf5 | ||
|
|
76d2206058 | ||
|
|
51e8b608dc | ||
|
|
a68330fb8f | ||
|
|
2449ad5c69 | ||
|
|
064c812df3 | ||
|
|
48d5ec9e66 | ||
|
|
914419fd1e | ||
|
|
f005fc8ca0 | ||
|
|
43b7960de2 | ||
|
|
2ed1e58032 | ||
|
|
c63b212700 | ||
|
|
e9df78c0e7 | ||
|
|
b0daf81185 | ||
|
|
cee4fdcbb0 | ||
|
|
df3cdfccb0 | ||
|
|
894042cd0e | ||
|
|
8123287952 | ||
|
|
bc677705d8 | ||
|
|
5dd8ea8aab | ||
|
|
41172be796 | ||
|
|
ad1b4a9a86 | ||
|
|
e0e3ec02b3 | ||
|
|
a6cc392473 | ||
|
|
36f48b8656 | ||
|
|
3d883ca37d | ||
|
|
34ed81ca64 | ||
|
|
a9e0880572 | ||
|
|
9500e1c3c4 | ||
|
|
d81aa9cbbc | ||
|
|
21d4b25c2d | ||
|
|
0080783a11 | ||
|
|
2c3f44a3f8 | ||
|
|
3ddf414097 | ||
|
|
59fb63f1f7 | ||
|
|
fa1b514440 | ||
|
|
1579c58876 | ||
|
|
153d044331 | ||
|
|
f2496f7054 | ||
|
|
99022f4f3d | ||
|
|
60a5e4f261 | ||
|
|
661586d3b6 | ||
|
|
abc26cf906 | ||
|
|
12351bada7 | ||
|
|
a6816d53d7 | ||
|
|
3b0709f5f2 | ||
|
|
d7af7e2917 | ||
|
|
6516e62d33 | ||
|
|
6b832edd2f | ||
|
|
eebace1652 | ||
|
|
6ff6e05408 | ||
|
|
aaf569ca8c | ||
|
|
31eef6222e | ||
|
|
9963afa558 | ||
|
|
5b2e2fcf9d | ||
|
|
cc746e59a1 | ||
|
|
2cdb1c519d | ||
|
|
426074ded9 | ||
|
|
772a096615 | ||
|
|
e113e011cb | ||
|
|
22266484bd | ||
|
|
559c011420 | ||
|
|
411c0633a3 | ||
|
|
488f023bdf | ||
|
|
22878f4ef8 | ||
|
|
e732a39fea | ||
|
|
62b4bf7af4 | ||
|
|
47a525ddb4 | ||
|
|
f4360725e0 | ||
|
|
b86607cd41 | ||
|
|
bf57de85c3 | ||
|
|
2dd6118ff4 | ||
|
|
816a53a7b1 | ||
|
|
ced93b0525 | ||
|
|
524ff9a4a6 | ||
|
|
f15032f905 | ||
|
|
d7d31a19e5 | ||
|
|
df2a7ddca4 | ||
|
|
ba9c71ffa4 | ||
|
|
21b6c6569c | ||
|
|
92aba9565a | ||
|
|
6ea0aebb0b | ||
|
|
b5cdcb75b4 | ||
|
|
bd9aae40b8 | ||
|
|
33f931c0a4 | ||
|
|
ede8279c17 | ||
|
|
268b84a2b6 | ||
|
|
0a67145d80 | ||
|
|
2e55bc470c | ||
|
|
cf0d038978 | ||
|
|
92e7db1082 | ||
|
|
c45c47f935 | ||
|
|
341e27f9a3 | ||
|
|
ab167175c9 | ||
|
|
3c2933338f | ||
|
|
829784fa50 | ||
|
|
3c45f8dc91 | ||
|
|
f8ebf7c6ad | ||
|
|
510c364607 | ||
|
|
a3d6fcccb7 | ||
|
|
42c8082edd | ||
|
|
1a7edf7f0e | ||
|
|
4760deaf9c | ||
|
|
0f7b9d02a0 | ||
|
|
adc86c7945 | ||
|
|
12969eda07 | ||
|
|
e07952455f | ||
|
|
4494230854 | ||
|
|
e8dd21c0c3 | ||
|
|
36ef1b2fd6 | ||
|
|
c3d2bd8ed1 | ||
|
|
da2b4be539 | ||
|
|
b5e11f85d5 | ||
|
|
9e1b2f8912 | ||
|
|
1e5f4b0267 | ||
|
|
4fedd03074 | ||
|
|
f6feaeea85 | ||
|
|
c8743c0ab7 | ||
|
|
3d80ed95ca | ||
|
|
0a28bfa9c2 | ||
|
|
6d771f77e6 | ||
|
|
717ca1bb18 | ||
|
|
4f3c48cb4f | ||
|
|
b1b02dc8e5 | ||
|
|
a060ff52ad | ||
|
|
42d73fe25d | ||
|
|
b5946344dc | ||
|
|
dd46e45aba | ||
|
|
61ee4549e1 | ||
|
|
9767f6244f | ||
|
|
0038d74b86 | ||
|
|
6b2163c61f | ||
|
|
56f976c6b5 | ||
|
|
3ee0bfe1ea | ||
|
|
cd9f003da1 | ||
|
|
c452524e3e | ||
|
|
13f98ddbd6 | ||
|
|
9a5c7c10de | ||
|
|
41998565db | ||
|
|
3c64a8eb18 | ||
|
|
962ba0b358 | ||
|
|
16780f91a3 | ||
|
|
5e5a06b0ff | ||
|
|
859e0f20b8 | ||
|
|
9a15d5ce4e | ||
|
|
e4bba28579 | ||
|
|
f3efddd849 | ||
|
|
39190f97d4 | ||
|
|
3b037c5011 | ||
|
|
79387d5396 | ||
|
|
62e747b74a | ||
|
|
9643aed8f8 | ||
|
|
f4fb9e3ab4 | ||
|
|
30487e6108 | ||
|
|
fd2d285af5 | ||
|
|
87bbf59d87 | ||
|
|
37e954626d | ||
|
|
829c7d8be6 | ||
|
|
3274885803 | ||
|
|
c6153ea67d | ||
|
|
191bffedcb | ||
|
|
9ddda81372 | ||
|
|
ddb3c4e3ce | ||
|
|
c87d27630b | ||
|
|
c1d0bb830e | ||
|
|
93dde4c985 | ||
|
|
0eb1cbce43 | ||
|
|
a935c8bb35 | ||
|
|
03eea8ce15 | ||
|
|
76b1adebc4 | ||
|
|
3be8f685bd | ||
|
|
4a392395ab | ||
|
|
fd9755e4a0 | ||
|
|
34151b03ef | ||
|
|
f63205f86c | ||
|
|
5e5867528d | ||
|
|
05623b0e13 | ||
|
|
12602da16c | ||
|
|
2b6dee9949 | ||
|
|
11fa305508 | ||
|
|
b532a3e784 |
8
.github/workflows/publish.yml
vendored
8
.github/workflows/publish.yml
vendored
@@ -7,15 +7,19 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "pyproject.toml"
|
- "pyproject.toml"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-node:
|
publish-node:
|
||||||
name: Publish Custom Node to registry
|
name: Publish Custom Node to registry
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository_owner == 'ltdrdata' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Publish Custom Node
|
- name: Publish Custom Node
|
||||||
uses: Comfy-Org/publish-node-action@main
|
uses: Comfy-Org/publish-node-action@v1
|
||||||
with:
|
with:
|
||||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||||
|
|||||||
96
README.md
96
README.md
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
**ComfyUI-Manager** is an extension designed to enhance the usability of [ComfyUI](https://github.com/comfyanonymous/ComfyUI). It offers management functions to **install, remove, disable, and enable** various custom nodes of ComfyUI. Furthermore, this extension provides a hub feature and convenience functions to access a wide range of information within ComfyUI.
|
**ComfyUI-Manager** is an extension designed to enhance the usability of [ComfyUI](https://github.com/comfyanonymous/ComfyUI). It offers management functions to **install, remove, disable, and enable** various custom nodes of ComfyUI. Furthermore, this extension provides a hub feature and convenience functions to access a wide range of information within ComfyUI.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## NOTICE
|
## NOTICE
|
||||||
|
* V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`.
|
||||||
|
* V3.10: `double-click feature` is removed
|
||||||
|
* This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper
|
||||||
* V3.3.2: Overhauled. Officially supports [https://comfyregistry.org/](https://comfyregistry.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.
|
||||||
|
|
||||||
@@ -25,6 +28,7 @@ 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 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
|
||||||
|
|
||||||

|

|
||||||
@@ -50,6 +54,7 @@ python -m venv venv
|
|||||||
pip install comfy-cli
|
pip install comfy-cli
|
||||||
comfy install
|
comfy install
|
||||||
```
|
```
|
||||||
|
* 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)
|
||||||
@@ -58,6 +63,7 @@ To install ComfyUI with ComfyUI-Manager on Linux using a venv environment, you c
|
|||||||
* **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 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`
|
||||||
@@ -90,7 +96,7 @@ This repository provides Colab notebooks that allow you to install and use Comfy
|
|||||||
|
|
||||||
2. If you click on 'Install Custom Nodes' or 'Install Models', an installer dialog will open.
|
2. If you click on 'Install Custom Nodes' or 'Install Models', an installer dialog will open.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
* There are three DB modes: `DB: Channel (1day cache)`, `DB: Local`, and `DB: Channel (remote)`.
|
* There are three DB modes: `DB: Channel (1day cache)`, `DB: Local`, and `DB: Channel (remote)`.
|
||||||
* `Channel (1day cache)` utilizes Channel cache information with a validity period of one day to quickly display the list.
|
* `Channel (1day cache)` utilizes Channel cache information with a validity period of one day to quickly display the list.
|
||||||
@@ -143,10 +149,20 @@ In `ComfyUI-Manager` V3.0 and later, configuration files and dynamically generat
|
|||||||
* Basic config files: `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini`
|
* Basic config files: `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini`
|
||||||
* Configurable channel lists: `<USER_DIRECTORY>/default/ComfyUI-Manager/channels.ini`
|
* Configurable channel lists: `<USER_DIRECTORY>/default/ComfyUI-Manager/channels.ini`
|
||||||
* Configurable pip overrides: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_overrides.json`
|
* Configurable pip overrides: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_overrides.json`
|
||||||
|
* Configurable pip blacklist: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_blacklist.list`
|
||||||
|
* Configurable pip auto fix: `<USER_DIRECTORY>/default/ComfyUI-Manager/pip_auto_fix.list`
|
||||||
* Saved snapshot files: `<USER_DIRECTORY>/default/ComfyUI-Manager/snapshots`
|
* Saved snapshot files: `<USER_DIRECTORY>/default/ComfyUI-Manager/snapshots`
|
||||||
* Startup script files: `<USER_DIRECTORY>/default/ComfyUI-Manager/startup-scripts`
|
* Startup script files: `<USER_DIRECTORY>/default/ComfyUI-Manager/startup-scripts`
|
||||||
* Component files: `<USER_DIRECTORY>/default/ComfyUI-Manager/components`
|
* Component files: `<USER_DIRECTORY>/default/ComfyUI-Manager/components`
|
||||||
|
|
||||||
|
|
||||||
|
## `extra_model_paths.yaml` Configuration
|
||||||
|
The following settings are applied based on the section marked as `is_default`.
|
||||||
|
|
||||||
|
* `custom_nodes`: Path for installing custom nodes
|
||||||
|
* Importing does not need to adhere to the path set as `is_default`, but this is the path where custom nodes are installed by the `ComfyUI Nodes Manager`.
|
||||||
|
* `download_model_base`: Path for downloading models
|
||||||
|
|
||||||
|
|
||||||
## Snapshot-Manager
|
## Snapshot-Manager
|
||||||
* When you press `Save snapshot` or use `Update All` on `Manager Menu`, the current installation status snapshot is saved.
|
* When you press `Save snapshot` or use `Update All` on `Manager Menu`, the current installation status snapshot is saved.
|
||||||
@@ -174,17 +190,18 @@ In `ComfyUI-Manager` V3.0 and later, configuration files and dynamically generat
|
|||||||
|
|
||||||
## Custom node support guide
|
## Custom node support guide
|
||||||
|
|
||||||
|
* **NOTICE:**
|
||||||
|
- You should no longer assume that the GitHub repository name will match the subdirectory name under `custom_nodes`. The name of the subdirectory under `custom_nodes` will now use the normalized name from the `name` field in `pyproject.toml`.
|
||||||
|
- Avoid relying on directory names for imports whenever possible.
|
||||||
|
|
||||||
* https://docs.comfy.org/registry/overview
|
* https://docs.comfy.org/registry/overview
|
||||||
|
* https://github.com/Comfy-Org/rfcs
|
||||||
|
|
||||||
|
**Special purpose files** (optional)
|
||||||
* **Special purpose files** (optional)
|
* `pyproject.toml` - Spec file for comfyregistry.
|
||||||
* `node_list.json` - When your custom nodes pattern of NODE_CLASS_MAPPINGS is not conventional, it is used to manually provide a list of nodes for reference. ([example](https://github.com/melMass/comfy_mtb/raw/main/node_list.json))
|
* `node_list.json` - When your custom nodes pattern of NODE_CLASS_MAPPINGS is not conventional, it is used to manually provide a list of nodes for reference. ([example](https://github.com/melMass/comfy_mtb/raw/main/node_list.json))
|
||||||
* `requirements.txt` - When installing, this pip requirements will be installed automatically
|
* `requirements.txt` - When installing, this pip requirements will be installed automatically
|
||||||
* `install.py` - When installing, it is automatically called
|
* `install.py` - When installing, it is automatically called
|
||||||
* `uninstall.py` - When uninstalling, it is automatically called
|
|
||||||
* `disable.py` - When disabled, it is automatically called
|
|
||||||
* When installing a custom node setup `.js` file, it is recommended to write this script for disabling.
|
|
||||||
* `enable.py` - When enabled, it is automatically called
|
|
||||||
* **All scripts are executed from the root path of the corresponding custom node.**
|
* **All scripts are executed from the root path of the corresponding custom node.**
|
||||||
|
|
||||||
|
|
||||||
@@ -232,6 +249,32 @@ In `ComfyUI-Manager` V3.0 and later, configuration files and dynamically generat
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
# Config
|
||||||
|
* You can modify the `config.ini` file to apply the settings for ComfyUI-Manager.
|
||||||
|
* The path to the `config.ini` used by ComfyUI-Manager is displayed in the startup log messages.
|
||||||
|
* See also: [https://github.com/ltdrdata/ComfyUI-Manager#paths]
|
||||||
|
* Configuration options:
|
||||||
|
```
|
||||||
|
[default]
|
||||||
|
git_exe = <Manually specify the path to the git executable. If left empty, the default git executable path will be used.>
|
||||||
|
use_uv = <Use uv instead of pip for dependency installation.>
|
||||||
|
default_cache_as_channel_url = <Determines whether to retrieve the DB designated as channel_url at startup>
|
||||||
|
bypass_ssl = <Set to True if SSL errors occur to disable SSL.>
|
||||||
|
file_logging = <Configure whether to create a log file used by ComfyUI-Manager.>
|
||||||
|
windows_selector_event_loop_policy = <If an event loop error occurs on Windows, set this to True.>
|
||||||
|
model_download_by_agent = <When downloading models, use an agent instead of torchvision_download_url.>
|
||||||
|
downgrade_blacklist = <Set a list of packages to prevent downgrades. List them separated by commas.>
|
||||||
|
security_level = <Set the security level => strong|normal|normal-|weak>
|
||||||
|
always_lazy_install = <Whether to perform dependency installation on restart even in environments other than Windows.>
|
||||||
|
network_mode = <Set the network mode => public|private|offline>
|
||||||
|
```
|
||||||
|
|
||||||
|
* network_mode:
|
||||||
|
- public: An environment that uses a typical public network.
|
||||||
|
- private: An environment that uses a closed network, where a private node DB is configured via `channel_url`. (Uses cache if available)
|
||||||
|
- offline: An environment that does not use any external connections when using an offline network. (Uses cache if available)
|
||||||
|
|
||||||
|
|
||||||
## Additional Feature
|
## Additional Feature
|
||||||
* 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`.
|
||||||
@@ -260,12 +303,40 @@ In `ComfyUI-Manager` V3.0 and later, configuration files and dynamically generat
|
|||||||
* Custom pip mapping
|
* Custom pip mapping
|
||||||
* When you create the `pip_overrides.json` file, it changes the installation of specific pip packages to installations defined by the user.
|
* When you create the `pip_overrides.json` file, it changes the installation of specific pip packages to installations defined by the user.
|
||||||
* Please refer to the `pip_overrides.json.template` file.
|
* Please refer to the `pip_overrides.json.template` file.
|
||||||
|
|
||||||
|
* Prevent the installation of specific pip packages
|
||||||
|
* List the package names one per line in the `pip_blacklist.list` file.
|
||||||
|
|
||||||
|
* Automatically Restoring pip Installation
|
||||||
|
* If you list pip spec requirements in `pip_auto_fix.list`, similar to `requirements.txt`, it will automatically restore the specified versions when starting ComfyUI or when versions get mismatched during various custom node installations.
|
||||||
|
* `--index-url` can be used.
|
||||||
|
|
||||||
* Use `aria2` as downloader
|
* Use `aria2` as downloader
|
||||||
* [howto](docs/en/use_aria2.md)
|
* [howto](docs/en/use_aria2.md)
|
||||||
|
|
||||||
* If you add the item `skip_migration_check = True` to `config.ini`, it will not check whether there are nodes that can be migrated at startup.
|
|
||||||
* This option can be used if performance issues occur in a Colab+GDrive environment.
|
## Environment Variables
|
||||||
|
|
||||||
|
The following features can be configured using environment variables:
|
||||||
|
|
||||||
|
* **COMFYUI_PATH**: The installation path of ComfyUI
|
||||||
|
* **GITHUB_ENDPOINT**: Reverse proxy configuration for environments with limited access to GitHub
|
||||||
|
* **HF_ENDPOINT**: Reverse proxy configuration for environments with limited access to Hugging Face
|
||||||
|
|
||||||
|
|
||||||
|
### Example 1:
|
||||||
|
Redirecting `https://github.com/ltdrdata/ComfyUI-Impact-Pack` to `https://mirror.ghproxy.com/https://github.com/ltdrdata/ComfyUI-Impact-Pack`
|
||||||
|
|
||||||
|
```
|
||||||
|
GITHUB_ENDPOINT=https://mirror.ghproxy.com/https://github.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example 2:
|
||||||
|
Changing `https://huggingface.co/path/to/somewhere` to `https://some-hf-mirror.com/path/to/somewhere`
|
||||||
|
|
||||||
|
```
|
||||||
|
HF_ENDPOINT=https://some-hf-mirror.com
|
||||||
|
```
|
||||||
|
|
||||||
## Scanner
|
## Scanner
|
||||||
When you run the `scan.sh` script:
|
When you run the `scan.sh` script:
|
||||||
@@ -290,6 +361,7 @@ When you run the `scan.sh` script:
|
|||||||
* if `SSL: CERTIFICATE_VERIFY_FAILED` error is occured.
|
* if `SSL: CERTIFICATE_VERIFY_FAILED` error is occured.
|
||||||
* Edit `config.ini` file: add `bypass_ssl = True`
|
* Edit `config.ini` file: add `bypass_ssl = True`
|
||||||
|
|
||||||
|
|
||||||
## Security policy
|
## Security policy
|
||||||
* Edit `config.ini` file: add `security_level = <LEVEL>`
|
* Edit `config.ini` file: add `security_level = <LEVEL>`
|
||||||
* `strong`
|
* `strong`
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
This file is the entry point for the ComfyUI-Manager package, handling CLI-only mode and initial setup.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -7,7 +11,10 @@ if not os.path.exists(cli_mode_flag):
|
|||||||
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
|
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
|
||||||
import manager_server # noqa: F401
|
import manager_server # noqa: F401
|
||||||
import share_3rdparty # noqa: F401
|
import share_3rdparty # noqa: F401
|
||||||
WEB_DIRECTORY = "js"
|
import cm_global
|
||||||
|
|
||||||
|
if not cm_global.disable_front and not 'DISABLE_COMFYUI_MANAGER_FRONT' in os.environ:
|
||||||
|
WEB_DIRECTORY = "js"
|
||||||
else:
|
else:
|
||||||
print("\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n")
|
print("\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n")
|
||||||
|
|
||||||
|
|||||||
156
cm-cli.py
156
cm-cli.py
@@ -20,14 +20,19 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
|
|||||||
|
|
||||||
import manager_util
|
import manager_util
|
||||||
|
|
||||||
|
# read env vars
|
||||||
|
# COMFYUI_FOLDERS_BASE_PATH is not required in cm-cli.py
|
||||||
|
# `comfy_path` should be resolved before importing manager_core
|
||||||
comfy_path = os.environ.get('COMFYUI_PATH')
|
comfy_path = os.environ.get('COMFYUI_PATH')
|
||||||
if comfy_path is None:
|
if comfy_path is None:
|
||||||
try:
|
try:
|
||||||
import folder_paths
|
import folder_paths
|
||||||
comfy_path = os.path.join(os.path.dirname(folder_paths.__file__))
|
comfy_path = os.path.join(os.path.dirname(folder_paths.__file__))
|
||||||
except:
|
except:
|
||||||
|
print("\n[bold yellow]WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.[/bold yellow]", file=sys.stderr)
|
||||||
comfy_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..', '..'))
|
comfy_path = os.path.abspath(os.path.join(manager_util.comfyui_manager_path, '..', '..'))
|
||||||
|
|
||||||
|
# This should be placed here
|
||||||
sys.path.append(comfy_path)
|
sys.path.append(comfy_path)
|
||||||
|
|
||||||
import utils.extra_config
|
import utils.extra_config
|
||||||
@@ -36,33 +41,41 @@ import manager_core as core
|
|||||||
from manager_core import unified_manager
|
from manager_core import unified_manager
|
||||||
import cnr_utils
|
import cnr_utils
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
|
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
|
||||||
comfy_path = os.environ.get('COMFYUI_PATH')
|
|
||||||
|
|
||||||
if comfy_path is None:
|
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
|
||||||
print("\n[bold yellow]WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.[/bold yellow]", file=sys.stderr)
|
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||||
comfy_path = os.path.abspath(os.path.join(comfyui_manager_path, '..', '..'))
|
|
||||||
|
|
||||||
|
if sys.version_info < (3, 13):
|
||||||
cm_global.pip_blacklist = ['torch', 'torchsde', 'torchvision']
|
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
else:
|
||||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
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:
|
||||||
cm_global.pip_overrides = json.load(json_file)
|
cm_global.pip_overrides = json.load(json_file)
|
||||||
|
|
||||||
|
|
||||||
|
if os.path.exists(os.path.join(manager_util.comfyui_manager_path, "pip_blacklist.list")):
|
||||||
|
with open(os.path.join(manager_util.comfyui_manager_path, "pip_blacklist.list"), 'r', encoding="UTF-8", errors="ignore") as f:
|
||||||
|
for x in f.readlines():
|
||||||
|
y = x.strip()
|
||||||
|
if y != '':
|
||||||
|
cm_global.pip_blacklist.add(y)
|
||||||
|
|
||||||
|
|
||||||
def check_comfyui_hash():
|
def check_comfyui_hash():
|
||||||
repo = git.Repo(comfy_path)
|
try:
|
||||||
core.comfy_ui_revision = len(list(repo.iter_commits('HEAD')))
|
repo = git.Repo(comfy_path)
|
||||||
|
core.comfy_ui_revision = len(list(repo.iter_commits('HEAD')))
|
||||||
|
core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
|
||||||
|
except:
|
||||||
|
print('[bold yellow]INFO: Frozen ComfyUI mode.[/bold yellow]')
|
||||||
|
core.comfy_ui_revision = 0
|
||||||
|
core.comfy_ui_commit_datetime = 0
|
||||||
|
|
||||||
cm_global.variables['comfyui.revision'] = core.comfy_ui_revision
|
cm_global.variables['comfyui.revision'] = core.comfy_ui_revision
|
||||||
|
|
||||||
core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime
|
|
||||||
|
|
||||||
|
|
||||||
check_comfyui_hash() # This is a preparation step for manager_core
|
check_comfyui_hash() # This is a preparation step for manager_core
|
||||||
core.check_invalid_nodes()
|
core.check_invalid_nodes()
|
||||||
@@ -71,7 +84,7 @@ core.check_invalid_nodes()
|
|||||||
def read_downgrade_blacklist():
|
def read_downgrade_blacklist():
|
||||||
try:
|
try:
|
||||||
import configparser
|
import configparser
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser(strict=False)
|
||||||
config.read(core.manager_config.path)
|
config.read(core.manager_config.path)
|
||||||
default_conf = config['default']
|
default_conf = config['default']
|
||||||
|
|
||||||
@@ -95,7 +108,8 @@ class Ctx:
|
|||||||
self.no_deps = False
|
self.no_deps = False
|
||||||
self.mode = 'cache'
|
self.mode = 'cache'
|
||||||
self.user_directory = None
|
self.user_directory = None
|
||||||
self.custom_nodes_paths = [os.path.join(core.comfy_path, 'custom_nodes')]
|
self.custom_nodes_paths = [os.path.join(core.comfy_base_path, 'custom_nodes')]
|
||||||
|
self.manager_files_directory = os.path.dirname(__file__)
|
||||||
|
|
||||||
if Ctx.folder_paths is None:
|
if Ctx.folder_paths is None:
|
||||||
try:
|
try:
|
||||||
@@ -118,7 +132,7 @@ class Ctx:
|
|||||||
if channel is not None:
|
if channel is not None:
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
|
|
||||||
asyncio.run(unified_manager.reload(cache_mode=self.mode == 'cache', dont_wait=False))
|
asyncio.run(unified_manager.reload(cache_mode=self.mode, dont_wait=False))
|
||||||
asyncio.run(unified_manager.load_nightly(self.channel, self.mode))
|
asyncio.run(unified_manager.load_nightly(self.channel, self.mode))
|
||||||
|
|
||||||
def set_no_deps(self, no_deps):
|
def set_no_deps(self, no_deps):
|
||||||
@@ -137,7 +151,21 @@ class Ctx:
|
|||||||
if os.path.exists(core.manager_pip_overrides_path):
|
if os.path.exists(core.manager_pip_overrides_path):
|
||||||
with open(core.manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
|
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)
|
||||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
|
||||||
|
if sys.version_info < (3, 13):
|
||||||
|
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||||
|
|
||||||
|
if os.path.exists(core.manager_pip_blacklist_path):
|
||||||
|
with open(core.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
|
||||||
|
for x in f.readlines():
|
||||||
|
y = x.strip()
|
||||||
|
if y != '':
|
||||||
|
cm_global.pip_blacklist.add(y)
|
||||||
|
|
||||||
|
def update_custom_nodes_dir(self, target_dir):
|
||||||
|
import folder_paths
|
||||||
|
a, b = folder_paths.folder_names_and_paths['custom_nodes']
|
||||||
|
folder_paths.folder_names_and_paths['custom_nodes'] = [os.path.abspath(target_dir)], set()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_startup_scripts_path():
|
def get_startup_scripts_path():
|
||||||
@@ -162,13 +190,18 @@ class Ctx:
|
|||||||
cmd_ctx = Ctx()
|
cmd_ctx = Ctx()
|
||||||
|
|
||||||
|
|
||||||
def install_node(node_spec_str, is_all=False, cnt_msg=''):
|
def install_node(node_spec_str, is_all=False, cnt_msg='', **kwargs):
|
||||||
|
exit_on_fail = kwargs.get('exit_on_fail', False)
|
||||||
|
print(f"install_node exit on fail:{exit_on_fail}...")
|
||||||
|
|
||||||
if core.is_valid_url(node_spec_str):
|
if core.is_valid_url(node_spec_str):
|
||||||
# install via urls
|
# install via urls
|
||||||
res = asyncio.run(core.gitclone_install(node_spec_str, no_deps=cmd_ctx.no_deps))
|
res = asyncio.run(core.gitclone_install(node_spec_str, no_deps=cmd_ctx.no_deps))
|
||||||
if not res.result:
|
if not res.result:
|
||||||
print(res.msg)
|
print(res.msg)
|
||||||
print(f"[bold red]ERROR: An error occurred while installing '{node_spec_str}'.[/bold red]")
|
print(f"[bold red]ERROR: An error occurred while installing '{node_spec_str}'.[/bold red]")
|
||||||
|
if exit_on_fail:
|
||||||
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
print(f"{cnt_msg} [INSTALLED] {node_spec_str:50}")
|
print(f"{cnt_msg} [INSTALLED] {node_spec_str:50}")
|
||||||
else:
|
else:
|
||||||
@@ -203,6 +236,8 @@ def install_node(node_spec_str, is_all=False, cnt_msg=''):
|
|||||||
print("")
|
print("")
|
||||||
else:
|
else:
|
||||||
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.\n{res.msg}[/bold red]")
|
print(f"[bold red]ERROR: An error occurred while installing '{node_name}'.\n{res.msg}[/bold red]")
|
||||||
|
if exit_on_fail:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def reinstall_node(node_spec_str, is_all=False, cnt_msg=''):
|
def reinstall_node(node_spec_str, is_all=False, cnt_msg=''):
|
||||||
@@ -232,7 +267,7 @@ def fix_node(node_spec_str, is_all=False, cnt_msg=''):
|
|||||||
res = unified_manager.unified_fix(node_name, version_spec, no_deps=cmd_ctx.no_deps)
|
res = unified_manager.unified_fix(node_name, version_spec, no_deps=cmd_ctx.no_deps)
|
||||||
|
|
||||||
if not res.result:
|
if not res.result:
|
||||||
print(f"ERROR: f{res.msg}")
|
print(f"[bold red]ERROR: f{res.msg}[/bold red]")
|
||||||
|
|
||||||
|
|
||||||
def uninstall_node(node_spec_str: str, is_all: bool = False, cnt_msg: str = ''):
|
def uninstall_node(node_spec_str: str, is_all: bool = False, cnt_msg: str = ''):
|
||||||
@@ -537,7 +572,7 @@ def get_all_installed_node_specs():
|
|||||||
res.append(node_spec_str)
|
res.append(node_spec_str)
|
||||||
processed.add(k)
|
processed.add(k)
|
||||||
|
|
||||||
for k, _ in unified_manager.cnr_inactive_nodes.keys():
|
for k in unified_manager.cnr_inactive_nodes.keys():
|
||||||
if k in processed:
|
if k in processed:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -546,7 +581,7 @@ def get_all_installed_node_specs():
|
|||||||
node_spec_str = f"{k}@{str(latest[0])}"
|
node_spec_str = f"{k}@{str(latest[0])}"
|
||||||
res.append(node_spec_str)
|
res.append(node_spec_str)
|
||||||
|
|
||||||
for k, _ in unified_manager.nightly_inactive_nodes.keys():
|
for k in unified_manager.nightly_inactive_nodes.keys():
|
||||||
if k in processed:
|
if k in processed:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -564,7 +599,7 @@ def get_all_installed_node_specs():
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def for_each_nodes(nodes, act, allow_all=True):
|
def for_each_nodes(nodes, act, allow_all=True, **kwargs):
|
||||||
is_all = False
|
is_all = False
|
||||||
if allow_all and 'all' in nodes:
|
if allow_all and 'all' in nodes:
|
||||||
is_all = True
|
is_all = True
|
||||||
@@ -576,7 +611,7 @@ def for_each_nodes(nodes, act, allow_all=True):
|
|||||||
i = 1
|
i = 1
|
||||||
for x in nodes:
|
for x in nodes:
|
||||||
try:
|
try:
|
||||||
act(x, is_all=is_all, cnt_msg=f'{i}/{total}')
|
act(x, is_all=is_all, cnt_msg=f'{i}/{total}', **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ERROR: {e}")
|
print(f"ERROR: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -620,11 +655,18 @@ def install(
|
|||||||
None,
|
None,
|
||||||
help="user directory"
|
help="user directory"
|
||||||
),
|
),
|
||||||
|
exit_on_fail: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
help="Exit on failure"
|
||||||
|
)
|
||||||
):
|
):
|
||||||
cmd_ctx.set_user_directory(user_directory)
|
cmd_ctx.set_user_directory(user_directory)
|
||||||
cmd_ctx.set_channel_mode(channel, mode)
|
cmd_ctx.set_channel_mode(channel, mode)
|
||||||
cmd_ctx.set_no_deps(no_deps)
|
cmd_ctx.set_no_deps(no_deps)
|
||||||
for_each_nodes(nodes, act=install_node)
|
|
||||||
|
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
|
||||||
|
for_each_nodes(nodes, act=install_node, exit_on_fail=exit_on_fail)
|
||||||
|
pip_fixer.fix_broken()
|
||||||
|
|
||||||
|
|
||||||
@app.command(help="Reinstall custom nodes")
|
@app.command(help="Reinstall custom nodes")
|
||||||
@@ -659,7 +701,10 @@ def reinstall(
|
|||||||
cmd_ctx.set_user_directory(user_directory)
|
cmd_ctx.set_user_directory(user_directory)
|
||||||
cmd_ctx.set_channel_mode(channel, mode)
|
cmd_ctx.set_channel_mode(channel, mode)
|
||||||
cmd_ctx.set_no_deps(no_deps)
|
cmd_ctx.set_no_deps(no_deps)
|
||||||
|
|
||||||
|
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
|
||||||
for_each_nodes(nodes, act=reinstall_node)
|
for_each_nodes(nodes, act=reinstall_node)
|
||||||
|
pip_fixer.fix_broken()
|
||||||
|
|
||||||
|
|
||||||
@app.command(help="Uninstall custom nodes")
|
@app.command(help="Uninstall custom nodes")
|
||||||
@@ -683,7 +728,7 @@ def uninstall(
|
|||||||
for_each_nodes(nodes, act=uninstall_node)
|
for_each_nodes(nodes, act=uninstall_node)
|
||||||
|
|
||||||
|
|
||||||
@app.command(help="Disable custom nodes")
|
@app.command(help="Update custom nodes")
|
||||||
def update(
|
def update(
|
||||||
nodes: List[str] = typer.Argument(
|
nodes: List[str] = typer.Argument(
|
||||||
...,
|
...,
|
||||||
@@ -711,12 +756,15 @@ def update(
|
|||||||
if 'all' in nodes:
|
if 'all' in nodes:
|
||||||
asyncio.run(auto_save_snapshot())
|
asyncio.run(auto_save_snapshot())
|
||||||
|
|
||||||
|
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
|
||||||
|
|
||||||
for x in nodes:
|
for x in nodes:
|
||||||
if x.lower() in ['comfyui', 'comfy', 'all']:
|
if x.lower() in ['comfyui', 'comfy', 'all']:
|
||||||
update_comfyui()
|
update_comfyui()
|
||||||
break
|
break
|
||||||
|
|
||||||
update_parallel(nodes)
|
update_parallel(nodes)
|
||||||
|
pip_fixer.fix_broken()
|
||||||
|
|
||||||
|
|
||||||
@app.command(help="Disable custom nodes")
|
@app.command(help="Disable custom nodes")
|
||||||
@@ -809,7 +857,9 @@ def fix(
|
|||||||
if 'all' in nodes:
|
if 'all' in nodes:
|
||||||
asyncio.run(auto_save_snapshot())
|
asyncio.run(auto_save_snapshot())
|
||||||
|
|
||||||
|
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
|
||||||
for_each_nodes(nodes, fix_node, allow_all=True)
|
for_each_nodes(nodes, fix_node, allow_all=True)
|
||||||
|
pip_fixer.fix_broken()
|
||||||
|
|
||||||
|
|
||||||
@app.command("show-versions", help="Show all available versions of the node")
|
@app.command("show-versions", help="Show all available versions of the node")
|
||||||
@@ -1004,17 +1054,34 @@ def save_snapshot(
|
|||||||
user_directory: str = typer.Option(
|
user_directory: str = typer.Option(
|
||||||
None,
|
None,
|
||||||
help="user directory"
|
help="user directory"
|
||||||
)
|
),
|
||||||
|
full_snapshot: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
show_default=False, help="If the snapshot should include custom node, ComfyUI version and pip versions (default), or only custom node details"
|
||||||
|
),
|
||||||
|
] = True,
|
||||||
):
|
):
|
||||||
cmd_ctx.set_user_directory(user_directory)
|
cmd_ctx.set_user_directory(user_directory)
|
||||||
|
|
||||||
path = asyncio.run(core.save_snapshot_with_postfix('snapshot', output))
|
if output is not None:
|
||||||
|
if(not output.endswith('.json') and not output.endswith('.yaml')):
|
||||||
|
print("[bold red]ERROR: output path should be either '.json' or '.yaml' file.[/bold red]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
dir_path = os.path.dirname(output)
|
||||||
|
|
||||||
|
if(dir_path != '' and not os.path.exists(dir_path)):
|
||||||
|
print(f"[bold red]ERROR: {output} path not exists.[/bold red]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
path = asyncio.run(core.save_snapshot_with_postfix('snapshot', output, not full_snapshot))
|
||||||
print(f"Current snapshot is saved as `{path}`")
|
print(f"Current snapshot is saved as `{path}`")
|
||||||
|
|
||||||
|
|
||||||
@app.command("restore-snapshot", help="Restore snapshot from snapshot file")
|
@app.command("restore-snapshot", help="Restore snapshot from snapshot file")
|
||||||
def restore_snapshot(
|
def restore_snapshot(
|
||||||
snapshot_name: str,
|
snapshot_name: str,
|
||||||
pip_non_url: Optional[bool] = typer.Option(
|
pip_non_url: Optional[bool] = typer.Option(
|
||||||
default=None,
|
default=None,
|
||||||
show_default=False,
|
show_default=False,
|
||||||
@@ -1036,10 +1103,17 @@ def restore_snapshot(
|
|||||||
user_directory: str = typer.Option(
|
user_directory: str = typer.Option(
|
||||||
None,
|
None,
|
||||||
help="user directory"
|
help="user directory"
|
||||||
|
),
|
||||||
|
restore_to: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
help="Manually specify the installation path for the custom node. Ignore user directory."
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
cmd_ctx.set_user_directory(user_directory)
|
cmd_ctx.set_user_directory(user_directory)
|
||||||
|
|
||||||
|
if restore_to:
|
||||||
|
cmd_ctx.update_custom_nodes_dir(restore_to)
|
||||||
|
|
||||||
extras = []
|
extras = []
|
||||||
if pip_non_url:
|
if pip_non_url:
|
||||||
extras.append('--pip-non-url')
|
extras.append('--pip-non-url')
|
||||||
@@ -1060,12 +1134,14 @@ def restore_snapshot(
|
|||||||
print(f"[bold red]ERROR: `{snapshot_path}` is not exists.[/bold red]")
|
print(f"[bold red]ERROR: `{snapshot_path}` is not exists.[/bold red]")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
|
||||||
try:
|
try:
|
||||||
asyncio.run(core.restore_snapshot(snapshot_path, extras))
|
asyncio.run(core.restore_snapshot(snapshot_path, extras))
|
||||||
except Exception:
|
except Exception:
|
||||||
print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
|
print("[bold red]ERROR: Failed to restore snapshot.[/bold red]")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise typer.Exit(code=1)
|
raise typer.Exit(code=1)
|
||||||
|
pip_fixer.fix_broken()
|
||||||
|
|
||||||
|
|
||||||
@app.command(
|
@app.command(
|
||||||
@@ -1089,11 +1165,14 @@ def restore_dependencies(
|
|||||||
|
|
||||||
total = len(node_paths)
|
total = len(node_paths)
|
||||||
i = 1
|
i = 1
|
||||||
|
|
||||||
|
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
|
||||||
for x in node_paths:
|
for x in node_paths:
|
||||||
print("----------------------------------------------------------------------------------------------------")
|
print("----------------------------------------------------------------------------------------------------")
|
||||||
print(f"Restoring [{i}/{total}]: {x}")
|
print(f"Restoring [{i}/{total}]: {x}")
|
||||||
unified_manager.execute_install_script('', x, instant_execution=True)
|
unified_manager.execute_install_script('', x, instant_execution=True)
|
||||||
i += 1
|
i += 1
|
||||||
|
pip_fixer.fix_broken()
|
||||||
|
|
||||||
|
|
||||||
@app.command(
|
@app.command(
|
||||||
@@ -1105,7 +1184,10 @@ def post_install(
|
|||||||
)
|
)
|
||||||
):
|
):
|
||||||
path = os.path.expanduser(path)
|
path = os.path.expanduser(path)
|
||||||
|
|
||||||
|
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
|
||||||
unified_manager.execute_install_script('', path, instant_execution=True)
|
unified_manager.execute_install_script('', path, instant_execution=True)
|
||||||
|
pip_fixer.fix_broken()
|
||||||
|
|
||||||
|
|
||||||
@app.command(
|
@app.command(
|
||||||
@@ -1147,6 +1229,7 @@ def install_deps(
|
|||||||
print(f"[bold red]Invalid json file: {deps}[/bold red]")
|
print(f"[bold red]Invalid json file: {deps}[/bold red]")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, core.manager_files_path)
|
||||||
for k in json_obj['custom_nodes'].keys():
|
for k in json_obj['custom_nodes'].keys():
|
||||||
state = core.simple_check_custom_node(k)
|
state = core.simple_check_custom_node(k)
|
||||||
if state == 'installed':
|
if state == 'installed':
|
||||||
@@ -1155,6 +1238,7 @@ def install_deps(
|
|||||||
asyncio.run(core.gitclone_install(k, instant_execution=True))
|
asyncio.run(core.gitclone_install(k, instant_execution=True))
|
||||||
else: # disabled
|
else: # disabled
|
||||||
core.gitclone_set_active([k], False)
|
core.gitclone_set_active([k], False)
|
||||||
|
pip_fixer.fix_broken()
|
||||||
|
|
||||||
print("Dependency installation and activation complete.")
|
print("Dependency installation and activation complete.")
|
||||||
|
|
||||||
@@ -1202,20 +1286,6 @@ def export_custom_node_ids(
|
|||||||
print(f"{x['id']}@unknown", file=output_file)
|
print(f"{x['id']}@unknown", file=output_file)
|
||||||
|
|
||||||
|
|
||||||
@app.command(
|
|
||||||
"migrate",
|
|
||||||
help="Migrate legacy node system to new node system",
|
|
||||||
)
|
|
||||||
def migrate(
|
|
||||||
user_directory: str = typer.Option(
|
|
||||||
None,
|
|
||||||
help="user directory"
|
|
||||||
)
|
|
||||||
):
|
|
||||||
cmd_ctx.set_user_directory(user_directory)
|
|
||||||
asyncio.run(unified_manager.migrate_unmanaged_nodes())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
sys.exit(app())
|
sys.exit(app())
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -121,8 +121,9 @@ ComfyUI-Loopchain
|
|||||||
* If no file exists at the snapshot path, it is implicitly assumed to be in ComfyUI-Manager/snapshots.
|
* If no file exists at the snapshot path, it is implicitly assumed to be in ComfyUI-Manager/snapshots.
|
||||||
* `--pip-non-url`: Restore for pip packages registered on PyPI.
|
* `--pip-non-url`: Restore for pip packages registered on PyPI.
|
||||||
* `--pip-non-local-url`: Restore for pip packages registered at web URLs.
|
* `--pip-non-local-url`: Restore for pip packages registered at web URLs.
|
||||||
* `--pip-local-url`: Restore for pip packages specified by local paths.
|
* `--pip-local-url`: Restore for pip packages specified by local paths.
|
||||||
|
* `--user-directory`: Set the user directory.
|
||||||
|
* `--restore-to`: The path where the restored custom nodes will be installed. (When this option is applied, only the custom nodes installed in the target path are recognized as installed.)
|
||||||
|
|
||||||
### 5. CLI Only Mode
|
### 5. CLI Only Mode
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ ComfyUI-Loopchain
|
|||||||
* `--pip-non-url`: PyPI 에 등록된 pip 패키지들에 대해서 복구를 수행
|
* `--pip-non-url`: PyPI 에 등록된 pip 패키지들에 대해서 복구를 수행
|
||||||
* `--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`: 사용자 디렉토리 설정
|
||||||
|
* `--restore-to`: 복구될 커스텀 노드가 설치될 경로. (이 옵션을 적용할 경우 오직 대상 경로에 설치된 custom nodes 만 설치된 것으로 인식함.)
|
||||||
|
|
||||||
### 5. CLI only mode
|
### 5. CLI only mode
|
||||||
|
|
||||||
|
|||||||
10288
extension-node-map.json
10288
extension-node-map.json
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
|||||||
"comfyui-layerdiffuse",
|
"comfyui-layerdiffuse",
|
||||||
"comfyui-liveportraitkj",
|
"comfyui-liveportraitkj",
|
||||||
"aigodlike-comfyui-translation",
|
"aigodlike-comfyui-translation",
|
||||||
"comfyui-reactor-node",
|
"comfyui-reactor",
|
||||||
"comfyui_instantid",
|
"comfyui_instantid",
|
||||||
"sd-dynamic-thresholding",
|
"sd-dynamic-thresholding",
|
||||||
"pr-was-node-suite-comfyui-47064894",
|
"pr-was-node-suite-comfyui-47064894",
|
||||||
|
|||||||
@@ -154,14 +154,27 @@ def switch_to_default_branch(repo):
|
|||||||
repo.git.checkout(default_branch)
|
repo.git.checkout(default_branch)
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
|
# try checkout master
|
||||||
|
# try checkout main if failed
|
||||||
try:
|
try:
|
||||||
repo.git.checkout(repo.heads.master)
|
repo.git.checkout(repo.heads.master)
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
if remote_name is not None:
|
if remote_name is not None:
|
||||||
repo.git.checkout('-b', 'master', f'{remote_name}/master')
|
repo.git.checkout('-b', 'master', f'{remote_name}/master')
|
||||||
|
return True
|
||||||
except:
|
except:
|
||||||
pass
|
try:
|
||||||
|
repo.git.checkout(repo.heads.main)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
if remote_name is not None:
|
||||||
|
repo.git.checkout('-b', 'main', f'{remote_name}/main')
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
print("[ComfyUI Manager] Failed to switch to the default branch")
|
print("[ComfyUI Manager] Failed to switch to the default branch")
|
||||||
return False
|
return False
|
||||||
@@ -389,12 +402,13 @@ def apply_snapshot(path):
|
|||||||
git_custom_node_infos = info['git_custom_nodes']
|
git_custom_node_infos = info['git_custom_nodes']
|
||||||
file_custom_node_infos = info['file_custom_nodes']
|
file_custom_node_infos = info['file_custom_nodes']
|
||||||
|
|
||||||
checkout_comfyui_hash(comfyui_hash)
|
if comfyui_hash:
|
||||||
|
checkout_comfyui_hash(comfyui_hash)
|
||||||
checkout_custom_node_hash(git_custom_node_infos)
|
checkout_custom_node_hash(git_custom_node_infos)
|
||||||
invalidate_custom_node_file(file_custom_node_infos)
|
invalidate_custom_node_file(file_custom_node_infos)
|
||||||
|
|
||||||
print("APPLY SNAPSHOT: True")
|
print("APPLY SNAPSHOT: True")
|
||||||
if 'pips' in info:
|
if 'pips' in info and info['pips']:
|
||||||
return info['pips']
|
return info['pips']
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
11285
github-stats.json
11285
github-stats.json
File diff suppressed because it is too large
Load Diff
@@ -110,3 +110,8 @@ def add_on_revision_detected(k, f):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
variables['cm.on_revision_detected_handler'].append((k, f))
|
variables['cm.on_revision_detected_handler'].append((k, f))
|
||||||
|
|
||||||
|
|
||||||
|
error_dict = {}
|
||||||
|
|
||||||
|
disable_front = False
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import requests
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
import manager_core
|
||||||
import manager_util
|
import manager_util
|
||||||
|
import requests
|
||||||
import toml
|
import toml
|
||||||
import os
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
base_url = "https://api.comfy.org"
|
base_url = "https://api.comfy.org"
|
||||||
|
|
||||||
@@ -13,33 +18,98 @@ lock = asyncio.Lock()
|
|||||||
|
|
||||||
is_cache_loading = False
|
is_cache_loading = False
|
||||||
|
|
||||||
async def get_cnr_data(page=1, limit=1000, cache_mode=True, dont_wait=True):
|
async def get_cnr_data(cache_mode=True, dont_wait=True):
|
||||||
|
try:
|
||||||
|
return await _get_cnr_data(cache_mode, dont_wait)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
print("A timeout occurred during the fetch process from ComfyRegistry.")
|
||||||
|
return await _get_cnr_data(cache_mode=True, dont_wait=True) # timeout fallback
|
||||||
|
|
||||||
|
async def _get_cnr_data(cache_mode=True, dont_wait=True):
|
||||||
global is_cache_loading
|
global is_cache_loading
|
||||||
|
|
||||||
uri = f'{base_url}/nodes?page={page}&limit={limit}'
|
uri = f'{base_url}/nodes'
|
||||||
|
|
||||||
def touch(json_obj):
|
async def fetch_all():
|
||||||
for v in json_obj['nodes']:
|
remained = True
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
full_nodes = {}
|
||||||
|
|
||||||
|
|
||||||
|
# Determine form factor based on environment and platform
|
||||||
|
is_desktop = bool(os.environ.get('__COMFYUI_DESKTOP_VERSION__'))
|
||||||
|
system = platform.system().lower()
|
||||||
|
is_windows = system == 'windows'
|
||||||
|
is_mac = system == 'darwin'
|
||||||
|
is_linux = system == 'linux'
|
||||||
|
|
||||||
|
# Get ComfyUI version tag
|
||||||
|
if is_desktop:
|
||||||
|
# extract version from pyproject.toml instead of git tag
|
||||||
|
comfyui_ver = manager_core.get_current_comfyui_ver() or 'unknown'
|
||||||
|
else:
|
||||||
|
comfyui_ver = manager_core.get_comfyui_tag() or 'unknown'
|
||||||
|
|
||||||
|
if is_desktop:
|
||||||
|
if is_windows:
|
||||||
|
form_factor = 'desktop-win'
|
||||||
|
elif is_mac:
|
||||||
|
form_factor = 'desktop-mac'
|
||||||
|
else:
|
||||||
|
form_factor = 'other'
|
||||||
|
else:
|
||||||
|
if is_windows:
|
||||||
|
form_factor = 'git-windows'
|
||||||
|
elif is_mac:
|
||||||
|
form_factor = 'git-mac'
|
||||||
|
elif is_linux:
|
||||||
|
form_factor = 'git-linux'
|
||||||
|
else:
|
||||||
|
form_factor = 'other'
|
||||||
|
|
||||||
|
while remained:
|
||||||
|
# Add comfyui_version and form_factor to the API request
|
||||||
|
sub_uri = f'{base_url}/nodes?page={page}&limit=30&comfyui_version={comfyui_ver}&form_factor={form_factor}'
|
||||||
|
sub_json_obj = await asyncio.wait_for(manager_util.get_data_with_cache(sub_uri, cache_mode=False, silent=True, dont_cache=True), timeout=30)
|
||||||
|
remained = page < sub_json_obj['totalPages']
|
||||||
|
|
||||||
|
for x in sub_json_obj['nodes']:
|
||||||
|
full_nodes[x['id']] = x
|
||||||
|
|
||||||
|
if page % 5 == 0:
|
||||||
|
print(f"FETCH ComfyRegistry Data: {page}/{sub_json_obj['totalPages']}")
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("FETCH ComfyRegistry Data [DONE]")
|
||||||
|
|
||||||
|
for v in full_nodes.values():
|
||||||
if 'latest_version' not in v:
|
if 'latest_version' not in v:
|
||||||
v['latest_version'] = dict(version='nightly')
|
v['latest_version'] = dict(version='nightly')
|
||||||
|
|
||||||
|
return {'nodes': list(full_nodes.values())}
|
||||||
|
|
||||||
if cache_mode:
|
if cache_mode:
|
||||||
if dont_wait:
|
|
||||||
json_obj = await manager_util.get_data_with_cache(uri, cache_mode=cache_mode, dont_wait=True) # fallback
|
|
||||||
|
|
||||||
if 'nodes' in json_obj:
|
|
||||||
touch(json_obj)
|
|
||||||
return json_obj['nodes']
|
|
||||||
else:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
is_cache_loading = True
|
is_cache_loading = True
|
||||||
|
cache_state = manager_util.get_cache_state(uri)
|
||||||
|
|
||||||
|
if dont_wait:
|
||||||
|
if cache_state == 'not-cached':
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
print("[ComfyUI-Manager] The ComfyRegistry cache update is still in progress, so an outdated cache is being used.")
|
||||||
|
with open(manager_util.get_cache_path(uri), 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||||
|
return json.load(json_file)['nodes']
|
||||||
|
|
||||||
|
if cache_state == 'cached':
|
||||||
|
with open(manager_util.get_cache_path(uri), 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||||
|
return json.load(json_file)['nodes']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
json_obj = await manager_util.get_data_with_cache(uri, cache_mode=cache_mode)
|
json_obj = await fetch_all()
|
||||||
touch(json_obj)
|
manager_util.save_to_cache(uri, json_obj)
|
||||||
|
|
||||||
return json_obj['nodes']
|
return json_obj['nodes']
|
||||||
except:
|
except:
|
||||||
res = {}
|
res = {}
|
||||||
@@ -140,7 +210,10 @@ def read_cnr_info(fullpath):
|
|||||||
|
|
||||||
project = data.get('project', {})
|
project = data.get('project', {})
|
||||||
name = project.get('name').strip().lower()
|
name = project.get('name').strip().lower()
|
||||||
version = project.get('version')
|
|
||||||
|
# normalize version
|
||||||
|
# for example: 2.5 -> 2.5.0
|
||||||
|
version = str(manager_util.StrictVersion(project.get('version')))
|
||||||
|
|
||||||
urls = project.get('urls', {})
|
urls = project.get('urls', {})
|
||||||
repository = urls.get('Repository')
|
repository = urls.get('Repository')
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import os
|
|||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
|
|
||||||
|
GITHUB_ENDPOINT = os.getenv('GITHUB_ENDPOINT')
|
||||||
|
|
||||||
|
|
||||||
def is_git_repo(path: str) -> bool:
|
def is_git_repo(path: str) -> bool:
|
||||||
""" Check if the path is a git repository. """
|
""" Check if the path is a git repository. """
|
||||||
# NOTE: Checking it through `git.Repo` must be avoided.
|
# NOTE: Checking it through `git.Repo` must be avoided.
|
||||||
@@ -37,7 +40,8 @@ def git_url(fullpath):
|
|||||||
if not os.path.exists(git_config_path):
|
if not os.path.exists(git_config_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
# Set `strict=False` to allow duplicate `vscode-merge-base` sections, addressing <https://github.com/ltdrdata/ComfyUI-Manager/issues/1529>
|
||||||
|
config = configparser.ConfigParser(strict=False)
|
||||||
config.read(git_config_path)
|
config.read(git_config_path)
|
||||||
|
|
||||||
for k, v in config.items():
|
for k, v in config.items():
|
||||||
@@ -46,16 +50,36 @@ def git_url(fullpath):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def normalize_url(url) -> str:
|
def normalize_url(url) -> str:
|
||||||
url = url.replace("git@github.com:", "https://github.com/")
|
github_id = normalize_to_github_id(url)
|
||||||
if url.endswith('.git'):
|
if github_id is not None:
|
||||||
url = url[:-4]
|
url = f"https://github.com/{github_id}"
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def normalize_url_http(url) -> str:
|
|
||||||
url = url.replace("https://github.com/", "git@github.com:")
|
|
||||||
if url.endswith('.git'):
|
|
||||||
url = url[:-4]
|
|
||||||
|
|
||||||
return url
|
def normalize_to_github_id(url) -> str:
|
||||||
|
if 'github' in url or (GITHUB_ENDPOINT is not None and GITHUB_ENDPOINT in url):
|
||||||
|
author = os.path.basename(os.path.dirname(url))
|
||||||
|
|
||||||
|
if author.startswith('git@github.com:'):
|
||||||
|
author = author.split(':')[1]
|
||||||
|
|
||||||
|
repo_name = os.path.basename(url)
|
||||||
|
if repo_name.endswith('.git'):
|
||||||
|
repo_name = repo_name[:-4]
|
||||||
|
|
||||||
|
return f"{author}/{repo_name}"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_url_for_clone(url):
|
||||||
|
url = normalize_url(url)
|
||||||
|
|
||||||
|
if GITHUB_ENDPOINT is not None and url.startswith('https://github.com/'):
|
||||||
|
url = GITHUB_ENDPOINT + url[18:] # url[18:] -> remove `https://github.com`
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,16 @@ import os
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import urllib
|
import urllib
|
||||||
import sys
|
import sys
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from huggingface_hub import HfApi
|
||||||
|
from tqdm.auto import tqdm
|
||||||
|
|
||||||
|
|
||||||
aria2 = os.getenv('COMFYUI_MANAGER_ARIA2_SERVER')
|
aria2 = os.getenv('COMFYUI_MANAGER_ARIA2_SERVER')
|
||||||
HF_ENDPOINT = os.getenv('HF_ENDPOINT')
|
HF_ENDPOINT = os.getenv('HF_ENDPOINT')
|
||||||
|
|
||||||
|
|
||||||
if aria2 is not None:
|
if aria2 is not None:
|
||||||
secret = os.getenv('COMFYUI_MANAGER_ARIA2_SECRET')
|
secret = os.getenv('COMFYUI_MANAGER_ARIA2_SECRET')
|
||||||
url = urlparse(aria2)
|
url = urlparse(aria2)
|
||||||
@@ -16,7 +22,11 @@ if aria2 is not None:
|
|||||||
aria2 = aria2p.API(aria2p.Client(host=host, port=port, secret=secret))
|
aria2 = aria2p.API(aria2p.Client(host=host, port=port, secret=secret))
|
||||||
|
|
||||||
|
|
||||||
def basic_download_url(url, dest_folder, filename):
|
def basic_download_url(url, dest_folder: str, filename: str):
|
||||||
|
'''
|
||||||
|
Download file from url to dest_folder with filename
|
||||||
|
using requests library.
|
||||||
|
'''
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# Ensure the destination folder exists
|
# Ensure the destination folder exists
|
||||||
@@ -40,6 +50,7 @@ def basic_download_url(url, dest_folder, filename):
|
|||||||
def download_url(model_url: str, model_dir: str, filename: str):
|
def download_url(model_url: str, model_dir: str, filename: str):
|
||||||
if HF_ENDPOINT:
|
if HF_ENDPOINT:
|
||||||
model_url = model_url.replace('https://huggingface.co', HF_ENDPOINT)
|
model_url = model_url.replace('https://huggingface.co', HF_ENDPOINT)
|
||||||
|
logging.info(f"model_url replaced by HF_ENDPOINT, new = {model_url}")
|
||||||
if aria2:
|
if aria2:
|
||||||
return aria2_download_url(model_url, model_dir, filename)
|
return aria2_download_url(model_url, model_dir, filename)
|
||||||
else:
|
else:
|
||||||
@@ -111,4 +122,38 @@ def download_url_with_agent(url, save_path):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
print("Installation was successful.")
|
print("Installation was successful.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# NOTE: snapshot_download doesn't provide file size tqdm.
|
||||||
|
def download_repo_in_bytes(repo_id, local_dir):
|
||||||
|
api = HfApi()
|
||||||
|
repo_info = api.repo_info(repo_id=repo_id, files_metadata=True)
|
||||||
|
|
||||||
|
os.makedirs(local_dir, exist_ok=True)
|
||||||
|
|
||||||
|
total_size = 0
|
||||||
|
for file_info in repo_info.siblings:
|
||||||
|
if file_info.size is not None:
|
||||||
|
total_size += file_info.size
|
||||||
|
|
||||||
|
pbar = tqdm(total=total_size, unit="B", unit_scale=True, desc="Downloading")
|
||||||
|
|
||||||
|
for file_info in repo_info.siblings:
|
||||||
|
out_path = os.path.join(local_dir, file_info.rfilename)
|
||||||
|
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||||
|
|
||||||
|
if file_info.size is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
download_url = f"https://huggingface.co/{repo_id}/resolve/main/{file_info.rfilename}"
|
||||||
|
|
||||||
|
with requests.get(download_url, stream=True) as r, open(out_path, "wb") as f:
|
||||||
|
r.raise_for_status()
|
||||||
|
for chunk in r.iter_content(chunk_size=65536):
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
pbar.update(len(chunk))
|
||||||
|
|
||||||
|
pbar.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
description:
|
description:
|
||||||
`manager_util` is the lightest module shared across the prestartup_script, main code, and cm-cli of ComfyUI-Manager.
|
`manager_util` is the lightest module shared across the prestartup_script, main code, and cm-cli of ComfyUI-Manager.
|
||||||
"""
|
"""
|
||||||
|
import traceback
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import json
|
import json
|
||||||
@@ -12,6 +13,9 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
import platform
|
||||||
|
import shlex
|
||||||
|
import cm_global
|
||||||
|
|
||||||
|
|
||||||
cache_lock = threading.Lock()
|
cache_lock = threading.Lock()
|
||||||
@@ -19,6 +23,30 @@ cache_lock = threading.Lock()
|
|||||||
comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def add_python_path_to_env():
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
sep = ':'
|
||||||
|
else:
|
||||||
|
sep = ';'
|
||||||
|
|
||||||
|
os.environ['PATH'] = os.path.dirname(sys.executable)+sep+os.environ['PATH']
|
||||||
|
|
||||||
|
|
||||||
|
def make_pip_cmd(cmd):
|
||||||
|
if 'python_embeded' in sys.executable:
|
||||||
|
if use_uv:
|
||||||
|
return [sys.executable, '-s', '-m', 'uv', 'pip'] + cmd
|
||||||
|
else:
|
||||||
|
return [sys.executable, '-s', '-m', 'pip'] + cmd
|
||||||
|
else:
|
||||||
|
# FIXED: https://github.com/ltdrdata/ComfyUI-Manager/issues/1667
|
||||||
|
if use_uv:
|
||||||
|
return [sys.executable, '-m', 'uv', 'pip'] + cmd
|
||||||
|
else:
|
||||||
|
return [sys.executable, '-m', 'pip'] + cmd
|
||||||
|
|
||||||
# DON'T USE StrictVersion - cannot handle pre_release version
|
# DON'T USE StrictVersion - cannot handle pre_release version
|
||||||
# try:
|
# try:
|
||||||
@@ -122,7 +150,12 @@ async def get_data(uri, silent=False):
|
|||||||
with open(uri, "r", encoding="utf-8") as f:
|
with open(uri, "r", encoding="utf-8") as f:
|
||||||
json_text = f.read()
|
json_text = f.read()
|
||||||
|
|
||||||
json_obj = json.loads(json_text)
|
try:
|
||||||
|
json_obj = json.loads(json_text)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[ComfyUI-Manager] An error occurred while fetching '{uri}': {e}")
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
if not silent:
|
if not silent:
|
||||||
print(" [DONE]")
|
print(" [DONE]")
|
||||||
@@ -130,9 +163,34 @@ async def get_data(uri, silent=False):
|
|||||||
return json_obj
|
return json_obj
|
||||||
|
|
||||||
|
|
||||||
async def get_data_with_cache(uri, silent=False, cache_mode=True, dont_wait=False):
|
def get_cache_path(uri):
|
||||||
cache_uri = str(simple_hash(uri)) + '_' + os.path.basename(uri).replace('&', "_").replace('?', "_").replace('=', "_")
|
cache_uri = str(simple_hash(uri)) + '_' + os.path.basename(uri).replace('&', "_").replace('?', "_").replace('=', "_")
|
||||||
cache_uri = os.path.join(cache_dir, cache_uri+'.json')
|
return os.path.join(cache_dir, cache_uri+'.json')
|
||||||
|
|
||||||
|
|
||||||
|
def get_cache_state(uri):
|
||||||
|
cache_uri = get_cache_path(uri)
|
||||||
|
|
||||||
|
if not os.path.exists(cache_uri):
|
||||||
|
return "not-cached"
|
||||||
|
elif is_file_created_within_one_day(cache_uri):
|
||||||
|
return "cached"
|
||||||
|
|
||||||
|
return "expired"
|
||||||
|
|
||||||
|
|
||||||
|
def save_to_cache(uri, json_obj, silent=False):
|
||||||
|
cache_uri = get_cache_path(uri)
|
||||||
|
|
||||||
|
with cache_lock:
|
||||||
|
with open(cache_uri, "w", encoding='utf-8') as file:
|
||||||
|
json.dump(json_obj, file, indent=4, sort_keys=True)
|
||||||
|
if not silent:
|
||||||
|
logging.info(f"[ComfyUI-Manager] default cache updated: {uri}")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_data_with_cache(uri, silent=False, cache_mode=True, dont_wait=False, dont_cache=False):
|
||||||
|
cache_uri = get_cache_path(uri)
|
||||||
|
|
||||||
if cache_mode and dont_wait:
|
if cache_mode and dont_wait:
|
||||||
# NOTE: return the cache if possible, even if it is expired, so do not cache
|
# NOTE: return the cache if possible, even if it is expired, so do not cache
|
||||||
@@ -150,11 +208,12 @@ async def get_data_with_cache(uri, silent=False, cache_mode=True, dont_wait=Fals
|
|||||||
json_obj = await get_data(cache_uri, silent=silent)
|
json_obj = await get_data(cache_uri, silent=silent)
|
||||||
else:
|
else:
|
||||||
json_obj = await get_data(uri, silent=silent)
|
json_obj = await get_data(uri, silent=silent)
|
||||||
with cache_lock:
|
if not dont_cache:
|
||||||
with open(cache_uri, "w", encoding='utf-8') as file:
|
with cache_lock:
|
||||||
json.dump(json_obj, file, indent=4, sort_keys=True)
|
with open(cache_uri, "w", encoding='utf-8') as file:
|
||||||
if not silent:
|
json.dump(json_obj, file, indent=4, sort_keys=True)
|
||||||
print(f"[ComfyUI-Manager] default cache updated: {uri}")
|
if not silent:
|
||||||
|
logging.info(f"[ComfyUI-Manager] default cache updated: {uri}")
|
||||||
|
|
||||||
return json_obj
|
return json_obj
|
||||||
|
|
||||||
@@ -169,10 +228,10 @@ def extract_package_as_zip(file_path, extract_path):
|
|||||||
with zipfile.ZipFile(file_path, "r") as zip_ref:
|
with zipfile.ZipFile(file_path, "r") as zip_ref:
|
||||||
zip_ref.extractall(extract_path)
|
zip_ref.extractall(extract_path)
|
||||||
extracted_files = zip_ref.namelist()
|
extracted_files = zip_ref.namelist()
|
||||||
print(f"Extracted zip file to {extract_path}")
|
logging.info(f"Extracted zip file to {extract_path}")
|
||||||
return extracted_files
|
return extracted_files
|
||||||
except zipfile.BadZipFile:
|
except zipfile.BadZipFile:
|
||||||
print(f"File '{file_path}' is not a zip or is corrupted.")
|
logging.error(f"File '{file_path}' is not a zip or is corrupted.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -184,7 +243,7 @@ def get_installed_packages(renew=False):
|
|||||||
|
|
||||||
if renew or pip_map is None:
|
if renew or pip_map is None:
|
||||||
try:
|
try:
|
||||||
result = subprocess.check_output([sys.executable, '-m', 'pip', 'list'], universal_newlines=True)
|
result = subprocess.check_output(make_pip_cmd(['list']), universal_newlines=True)
|
||||||
|
|
||||||
pip_map = {}
|
pip_map = {}
|
||||||
for line in result.split('\n'):
|
for line in result.split('\n'):
|
||||||
@@ -194,10 +253,11 @@ def get_installed_packages(renew=False):
|
|||||||
if y[0] == 'Package' or y[0].startswith('-'):
|
if y[0] == 'Package' or y[0].startswith('-'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pip_map[y[0]] = y[1]
|
normalized_name = y[0].lower().replace('-', '_')
|
||||||
|
pip_map[normalized_name] = y[1]
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
print("[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.")
|
logging.error("[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.")
|
||||||
return set()
|
return {}
|
||||||
|
|
||||||
return pip_map
|
return pip_map
|
||||||
|
|
||||||
@@ -207,52 +267,96 @@ def clear_pip_cache():
|
|||||||
pip_map = None
|
pip_map = None
|
||||||
|
|
||||||
|
|
||||||
torch_torchvision_version_map = {
|
def parse_requirement_line(line):
|
||||||
'2.5.1': '0.20.1',
|
tokens = shlex.split(line)
|
||||||
'2.5.0': '0.20.0',
|
if not tokens:
|
||||||
'2.4.1': '0.19.1',
|
return None
|
||||||
'2.4.0': '0.19.0',
|
|
||||||
'2.3.1': '0.18.1',
|
package_spec = tokens[0]
|
||||||
'2.3.0': '0.18.0',
|
|
||||||
'2.2.2': '0.17.2',
|
pattern = re.compile(
|
||||||
'2.2.1': '0.17.1',
|
r'^(?P<package>[A-Za-z0-9_.+-]+)'
|
||||||
'2.2.0': '0.17.0',
|
r'(?P<operator>==|>=|<=|!=|~=|>|<)?'
|
||||||
'2.1.2': '0.16.2',
|
r'(?P<version>[A-Za-z0-9_.+-]*)$'
|
||||||
'2.1.1': '0.16.1',
|
)
|
||||||
'2.1.0': '0.16.0',
|
m = pattern.match(package_spec)
|
||||||
'2.0.1': '0.15.2',
|
if not m:
|
||||||
'2.0.0': '0.15.1',
|
return None
|
||||||
|
|
||||||
|
package = m.group('package')
|
||||||
|
operator = m.group('operator') or None
|
||||||
|
version = m.group('version') or None
|
||||||
|
|
||||||
|
index_url = None
|
||||||
|
if '--index-url' in tokens:
|
||||||
|
idx = tokens.index('--index-url')
|
||||||
|
if idx + 1 < len(tokens):
|
||||||
|
index_url = tokens[idx + 1]
|
||||||
|
|
||||||
|
res = {'package': package}
|
||||||
|
|
||||||
|
if operator is not None:
|
||||||
|
res['operator'] = operator
|
||||||
|
|
||||||
|
if version is not None:
|
||||||
|
res['version'] = StrictVersion(version)
|
||||||
|
|
||||||
|
if index_url is not None:
|
||||||
|
res['index_url'] = index_url
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
torch_torchvision_torchaudio_version_map = {
|
||||||
|
'2.7.0': ('0.22.0', '2.7.0'),
|
||||||
|
'2.6.0': ('0.21.0', '2.6.0'),
|
||||||
|
'2.5.1': ('0.20.0', '2.5.0'),
|
||||||
|
'2.5.0': ('0.20.0', '2.5.0'),
|
||||||
|
'2.4.1': ('0.19.1', '2.4.1'),
|
||||||
|
'2.4.0': ('0.19.0', '2.4.0'),
|
||||||
|
'2.3.1': ('0.18.1', '2.3.1'),
|
||||||
|
'2.3.0': ('0.18.0', '2.3.0'),
|
||||||
|
'2.2.2': ('0.17.2', '2.2.2'),
|
||||||
|
'2.2.1': ('0.17.1', '2.2.1'),
|
||||||
|
'2.2.0': ('0.17.0', '2.2.0'),
|
||||||
|
'2.1.2': ('0.16.2', '2.1.2'),
|
||||||
|
'2.1.1': ('0.16.1', '2.1.1'),
|
||||||
|
'2.1.0': ('0.16.0', '2.1.0'),
|
||||||
|
'2.0.1': ('0.15.2', '2.0.1'),
|
||||||
|
'2.0.0': ('0.15.1', '2.0.0'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PIPFixer:
|
class PIPFixer:
|
||||||
def __init__(self, prev_pip_versions):
|
def __init__(self, prev_pip_versions, comfyui_path, manager_files_path):
|
||||||
self.prev_pip_versions = { **prev_pip_versions }
|
self.prev_pip_versions = { **prev_pip_versions }
|
||||||
|
self.comfyui_path = comfyui_path
|
||||||
|
self.manager_files_path = manager_files_path
|
||||||
|
|
||||||
def torch_rollback(self):
|
def torch_rollback(self):
|
||||||
spec = self.prev_pip_versions['torch'].split('+')
|
spec = self.prev_pip_versions['torch'].split('+')
|
||||||
if len(spec) > 0:
|
if len(spec) > 0:
|
||||||
platform = spec[1]
|
platform = spec[1]
|
||||||
else:
|
else:
|
||||||
cmd = [sys.executable, '-m', 'pip', 'install', '--force', 'torch', 'torchvision', 'torchaudio']
|
cmd = make_pip_cmd(['install', '--force', 'torch', 'torchvision', 'torchaudio'])
|
||||||
subprocess.check_output(cmd, universal_newlines=True)
|
subprocess.check_output(cmd, universal_newlines=True)
|
||||||
print(cmd)
|
logging.error(cmd)
|
||||||
return
|
return
|
||||||
|
|
||||||
torch_ver = StrictVersion(spec[0])
|
torch_ver = StrictVersion(spec[0])
|
||||||
torch_ver = f"{torch_ver.major}.{torch_ver.minor}.{torch_ver.patch}"
|
torch_ver = f"{torch_ver.major}.{torch_ver.minor}.{torch_ver.patch}"
|
||||||
torchvision_ver = torch_torchvision_version_map.get(torch_ver)
|
torch_torchvision_torchaudio_ver = torch_torchvision_torchaudio_version_map.get(torch_ver)
|
||||||
|
|
||||||
if torchvision_ver is None:
|
if torch_torchvision_torchaudio_ver is None:
|
||||||
cmd = [sys.executable, '-m', 'pip', 'install', '--pre',
|
cmd = make_pip_cmd(['install', '--pre', 'torch', 'torchvision', 'torchaudio',
|
||||||
'torch', 'torchvision', 'torchaudio',
|
'--index-url', f"https://download.pytorch.org/whl/nightly/{platform}"])
|
||||||
'--index-url', f"https://download.pytorch.org/whl/nightly/{platform}"]
|
logging.info("[ComfyUI-Manager] restore PyTorch to nightly version")
|
||||||
print("[manager-core] restore PyTorch to nightly version")
|
|
||||||
else:
|
else:
|
||||||
cmd = [sys.executable, '-m', 'pip', 'install',
|
torchvision_ver, torchaudio_ver = torch_torchvision_torchaudio_ver
|
||||||
f'torch=={torch_ver}', f'torchvision=={torchvision_ver}', f"torchaudio=={torch_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}"]
|
'--index-url', f"https://download.pytorch.org/whl/{platform}"])
|
||||||
print(f"[manager-core] restore PyTorch to {torch_ver}+{platform}")
|
logging.info(f"[ComfyUI-Manager] restore PyTorch to {torch_ver}+{platform}")
|
||||||
|
|
||||||
subprocess.check_output(cmd, universal_newlines=True)
|
subprocess.check_output(cmd, universal_newlines=True)
|
||||||
|
|
||||||
@@ -262,23 +366,25 @@ class PIPFixer:
|
|||||||
# remove `comfy` python package
|
# remove `comfy` python package
|
||||||
try:
|
try:
|
||||||
if 'comfy' in new_pip_versions:
|
if 'comfy' in new_pip_versions:
|
||||||
cmd = [sys.executable, '-m', 'pip', 'uninstall', 'comfy']
|
cmd = make_pip_cmd(['uninstall', 'comfy'])
|
||||||
subprocess.check_output(cmd, universal_newlines=True)
|
subprocess.check_output(cmd, universal_newlines=True)
|
||||||
|
|
||||||
print("[manager-core] 'comfy' python package is uninstalled.\nWARN: The 'comfy' package is completely unrelated to ComfyUI and should never be installed as it causes conflicts with ComfyUI.")
|
logging.warning("[ComfyUI-Manager] 'comfy' python package is uninstalled.\nWARN: The 'comfy' package is completely unrelated to ComfyUI and should never be installed as it causes conflicts with ComfyUI.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[manager-core] Failed to uninstall `comfy` python package")
|
logging.error("[ComfyUI-Manager] Failed to uninstall `comfy` python package")
|
||||||
print(e)
|
logging.error(e)
|
||||||
|
|
||||||
# fix torch - reinstall torch packages if version is changed
|
# fix torch - reinstall torch packages if version is changed
|
||||||
try:
|
try:
|
||||||
if self.prev_pip_versions['torch'] != new_pip_versions['torch'] \
|
if 'torch' not in self.prev_pip_versions or 'torchvision' not in self.prev_pip_versions or 'torchaudio' not in self.prev_pip_versions:
|
||||||
|
logging.error("[ComfyUI-Manager] PyTorch is not installed")
|
||||||
|
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']:
|
||||||
self.torch_rollback()
|
self.torch_rollback()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[manager-core] Failed to restore PyTorch")
|
logging.error("[ComfyUI-Manager] Failed to restore PyTorch")
|
||||||
print(e)
|
logging.error(e)
|
||||||
|
|
||||||
# fix opencv
|
# fix opencv
|
||||||
try:
|
try:
|
||||||
@@ -306,24 +412,103 @@ class PIPFixer:
|
|||||||
|
|
||||||
if len(targets) > 0:
|
if len(targets) > 0:
|
||||||
for x in targets:
|
for x in targets:
|
||||||
cmd = [sys.executable, '-m', 'pip', '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)
|
||||||
|
|
||||||
print(f"[manager-core] 'opencv' dependencies were fixed: {targets}")
|
logging.info(f"[ComfyUI-Manager] 'opencv' dependencies were fixed: {targets}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[manager-core] Failed to restore opencv")
|
logging.error("[ComfyUI-Manager] Failed to restore opencv")
|
||||||
print(e)
|
logging.error(e)
|
||||||
|
|
||||||
# fix numpy
|
# fix numpy
|
||||||
try:
|
if sys.version_info >= (3, 13):
|
||||||
np = new_pip_versions.get('numpy')
|
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`.")
|
||||||
if np is not None:
|
else:
|
||||||
if StrictVersion(np) >= StrictVersion('2'):
|
try:
|
||||||
subprocess.check_output([sys.executable, '-m', 'pip', 'install', "numpy<2"], universal_newlines=True)
|
np = new_pip_versions.get('numpy')
|
||||||
except Exception as e:
|
if cm_global.pip_overrides.get('numpy') == 'numpy<2':
|
||||||
print("[manager-core] Failed to restore numpy")
|
if np is not None:
|
||||||
print(e)
|
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
|
||||||
|
try:
|
||||||
|
# NOTE: package name in requirements is 'comfyui-frontend-package'
|
||||||
|
# but, package name from `pip freeze` is 'comfyui_frontend_package'
|
||||||
|
# but, package name from `uv pip freeze` is 'comfyui-frontend-package'
|
||||||
|
#
|
||||||
|
# get_installed_packages returns normalized name (i.e. comfyui_frontend_package)
|
||||||
|
if 'comfyui_frontend_package' not in new_pip_versions:
|
||||||
|
requirements_path = os.path.join(self.comfyui_path, 'requirements.txt')
|
||||||
|
|
||||||
|
with open(requirements_path, 'r') as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
|
||||||
|
front_line = next((line.strip() for line in lines if line.startswith('comfyui-frontend-package')), None)
|
||||||
|
if front_line is None:
|
||||||
|
logging.info("[ComfyUI-Manager] Skipped fixing the 'comfyui-frontend-package' dependency because the ComfyUI is outdated.")
|
||||||
|
else:
|
||||||
|
cmd = make_pip_cmd(['install', front_line])
|
||||||
|
subprocess.check_output(cmd , universal_newlines=True)
|
||||||
|
logging.info("[ComfyUI-Manager] 'comfyui-frontend-package' dependency were fixed")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("[ComfyUI-Manager] Failed to restore comfyui-frontend-package")
|
||||||
|
logging.error(e)
|
||||||
|
|
||||||
|
# restore based on custom list
|
||||||
|
pip_auto_fix_path = os.path.join(self.manager_files_path, "pip_auto_fix.list")
|
||||||
|
if os.path.exists(pip_auto_fix_path):
|
||||||
|
with open(pip_auto_fix_path, 'r', encoding="UTF-8", errors="ignore") as f:
|
||||||
|
fixed_list = []
|
||||||
|
|
||||||
|
for x in f.readlines():
|
||||||
|
try:
|
||||||
|
parsed = parse_requirement_line(x)
|
||||||
|
need_to_reinstall = True
|
||||||
|
|
||||||
|
normalized_name = parsed['package'].lower().replace('-', '_')
|
||||||
|
if normalized_name in new_pip_versions:
|
||||||
|
if 'version' in parsed and 'operator' in parsed:
|
||||||
|
cur = StrictVersion(new_pip_versions[parsed['package']])
|
||||||
|
dest = parsed['version']
|
||||||
|
op = parsed['operator']
|
||||||
|
if cur == dest:
|
||||||
|
if op in ['==', '>=', '<=']:
|
||||||
|
need_to_reinstall = False
|
||||||
|
elif cur < dest:
|
||||||
|
if op in ['<=', '<', '~=', '!=']:
|
||||||
|
need_to_reinstall = False
|
||||||
|
elif cur > dest:
|
||||||
|
if op in ['>=', '>', '~=', '!=']:
|
||||||
|
need_to_reinstall = False
|
||||||
|
|
||||||
|
if need_to_reinstall:
|
||||||
|
cmd_args = ['install']
|
||||||
|
if 'version' in parsed and 'operator' in parsed:
|
||||||
|
cmd_args.append(parsed['package']+parsed['operator']+parsed['version'].version_string)
|
||||||
|
|
||||||
|
if 'index_url' in parsed:
|
||||||
|
cmd_args.append('--index-url')
|
||||||
|
cmd_args.append(parsed['index_url'])
|
||||||
|
|
||||||
|
cmd = make_pip_cmd(cmd_args)
|
||||||
|
subprocess.check_output(cmd, universal_newlines=True)
|
||||||
|
|
||||||
|
fixed_list.append(parsed['package'])
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
logging.error(f"[ComfyUI-Manager] Failed to restore '{x}'")
|
||||||
|
logging.error(e)
|
||||||
|
|
||||||
|
if len(fixed_list) > 0:
|
||||||
|
logging.info(f"[ComfyUI-Manager] dependencies in pip_auto_fix.json were fixed: {fixed_list}")
|
||||||
|
|
||||||
def sanitize(data):
|
def sanitize(data):
|
||||||
return data.replace("<", "<").replace(">", ">")
|
return data.replace("<", "<").replace(">", ">")
|
||||||
@@ -332,3 +517,23 @@ def sanitize(data):
|
|||||||
def sanitize_filename(input_string):
|
def sanitize_filename(input_string):
|
||||||
result_string = re.sub(r'[^a-zA-Z0-9_]', '_', input_string)
|
result_string = re.sub(r'[^a-zA-Z0-9_]', '_', input_string)
|
||||||
return result_string
|
return result_string
|
||||||
|
|
||||||
|
|
||||||
|
def robust_readlines(fullpath):
|
||||||
|
import chardet
|
||||||
|
try:
|
||||||
|
with open(fullpath, "r") as f:
|
||||||
|
return f.readlines()
|
||||||
|
except:
|
||||||
|
encoding = None
|
||||||
|
with open(fullpath, "rb") as f:
|
||||||
|
raw_data = f.read()
|
||||||
|
result = chardet.detect(raw_data)
|
||||||
|
encoding = result['encoding']
|
||||||
|
|
||||||
|
if encoding is not None:
|
||||||
|
with open(fullpath, "r", encoding=encoding) as f:
|
||||||
|
return f.readlines()
|
||||||
|
|
||||||
|
print(f"[ComfyUI-Manager] Failed to recognize encoding for: {fullpath}")
|
||||||
|
return []
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ import {
|
|||||||
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
||||||
import {
|
import {
|
||||||
free_models, install_pip, install_via_git_url, manager_instance,
|
free_models, install_pip, install_via_git_url, manager_instance,
|
||||||
rebootAPI, migrateAPI, setManagerInstance, show_message, customAlert, customPrompt } from "./common.js";
|
rebootAPI, setManagerInstance, show_message, customAlert, customPrompt,
|
||||||
|
infoToast, showTerminal, setNeedRestart
|
||||||
|
} from "./common.js";
|
||||||
import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js";
|
import { ComponentBuilderDialog, getPureName, load_components, set_component_policy } from "./components-manager.js";
|
||||||
import { CustomNodesManager } from "./custom-nodes-manager.js";
|
import { CustomNodesManager } from "./custom-nodes-manager.js";
|
||||||
import { ModelManager } from "./model-manager.js";
|
import { ModelManager } from "./model-manager.js";
|
||||||
import { set_double_click_policy } from "./node_fixer.js";
|
|
||||||
import { SnapshotManager } from "./snapshot.js";
|
import { SnapshotManager } from "./snapshot.js";
|
||||||
|
|
||||||
|
let manager_version = await getVersion();
|
||||||
|
|
||||||
var docStyle = document.createElement('style');
|
var docStyle = document.createElement('style');
|
||||||
docStyle.innerHTML = `
|
docStyle.innerHTML = `
|
||||||
.comfy-toast {
|
.comfy-toast {
|
||||||
@@ -41,7 +44,7 @@ docStyle.innerHTML = `
|
|||||||
|
|
||||||
#cm-manager-dialog {
|
#cm-manager-dialog {
|
||||||
width: 1000px;
|
width: 1000px;
|
||||||
height: 520px;
|
height: 455px;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -138,7 +141,7 @@ docStyle.innerHTML = `
|
|||||||
|
|
||||||
.cm-notice-board {
|
.cm-notice-board {
|
||||||
width: 290px;
|
width: 290px;
|
||||||
height: 270px;
|
height: 230px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
color: var(--input-text);
|
color: var(--input-text);
|
||||||
border: 1px solid var(--descrip-text);
|
border: 1px solid var(--descrip-text);
|
||||||
@@ -224,9 +227,13 @@ document.head.appendChild(docStyle);
|
|||||||
|
|
||||||
var update_comfyui_button = null;
|
var update_comfyui_button = null;
|
||||||
var switch_comfyui_button = null;
|
var switch_comfyui_button = null;
|
||||||
var fetch_updates_button = null;
|
|
||||||
var update_all_button = null;
|
var update_all_button = null;
|
||||||
|
var restart_stop_button = null;
|
||||||
|
var update_policy_combo = null;
|
||||||
|
|
||||||
let share_option = 'all';
|
let share_option = 'all';
|
||||||
|
var is_updating = false;
|
||||||
|
|
||||||
|
|
||||||
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
|
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
|
||||||
const style = `
|
const style = `
|
||||||
@@ -425,102 +432,56 @@ async function init_notice(notice) {
|
|||||||
|
|
||||||
await init_share_option();
|
await init_share_option();
|
||||||
|
|
||||||
async function fetchNicknames() {
|
|
||||||
const response1 = await api.fetchApi(`/customnode/getmappings?mode=nickname`);
|
|
||||||
const mappings = await response1.json();
|
|
||||||
|
|
||||||
let result = {};
|
async function set_inprogress_mode() {
|
||||||
let nickname_patterns = [];
|
update_comfyui_button.disabled = true;
|
||||||
|
update_comfyui_button.style.backgroundColor = "gray";
|
||||||
|
|
||||||
for (let i in mappings) {
|
update_all_button.disabled = true;
|
||||||
let item = mappings[i];
|
update_all_button.style.backgroundColor = "gray";
|
||||||
var nickname;
|
|
||||||
if (item[1].nickname) {
|
|
||||||
nickname = item[1].nickname;
|
|
||||||
}
|
|
||||||
else if (item[1].title) {
|
|
||||||
nickname = item[1].title;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
nickname = item[1].title_aux;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let j in item[0]) {
|
switch_comfyui_button.disabled = true;
|
||||||
result[item[0][j]] = nickname;
|
switch_comfyui_button.style.backgroundColor = "gray";
|
||||||
}
|
|
||||||
|
|
||||||
if(item[1].nodename_pattern) {
|
restart_stop_button.innerText = 'Stop';
|
||||||
nickname_patterns.push([item[1].nodename_pattern, nickname]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [result, nickname_patterns];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [nicknames, nickname_patterns] = await fetchNicknames();
|
|
||||||
|
|
||||||
function getNickname(node, nodename) {
|
async function reset_action_buttons() {
|
||||||
if(node.nickname) {
|
const isElectron = 'electronAPI' in window;
|
||||||
return node.nickname;
|
|
||||||
|
if(isElectron) {
|
||||||
|
update_all_button.innerText = "Update All Custom Nodes";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (nicknames[nodename]) {
|
update_all_button.innerText = "Update All";
|
||||||
node.nickname = nicknames[nodename];
|
|
||||||
}
|
|
||||||
else if(node.getInnerNodes) {
|
|
||||||
let pure_name = getPureName(node);
|
|
||||||
let groupNode = app.graph.extra?.groupNodes?.[pure_name];
|
|
||||||
if(groupNode) {
|
|
||||||
let packname = groupNode.packname;
|
|
||||||
node.nickname = packname;
|
|
||||||
}
|
|
||||||
return node.nickname;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for(let i in nickname_patterns) {
|
|
||||||
let item = nickname_patterns[i];
|
|
||||||
if(nodename.match(item[0])) {
|
|
||||||
node.nickname = item[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return node.nickname;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_comfyui_button.innerText = "Update ComfyUI";
|
||||||
|
switch_comfyui_button.innerText = "Switch ComfyUI";
|
||||||
|
restart_stop_button.innerText = 'Restart';
|
||||||
|
|
||||||
|
update_comfyui_button.disabled = false;
|
||||||
|
update_all_button.disabled = false;
|
||||||
|
switch_comfyui_button.disabled = false;
|
||||||
|
|
||||||
|
update_comfyui_button.style.backgroundColor = "";
|
||||||
|
update_all_button.style.backgroundColor = "";
|
||||||
|
switch_comfyui_button.style.backgroundColor = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateComfyUI() {
|
async function updateComfyUI() {
|
||||||
let prev_text = update_comfyui_button.innerText;
|
let prev_text = update_comfyui_button.innerText;
|
||||||
update_comfyui_button.innerText = "Updating ComfyUI...";
|
update_comfyui_button.innerText = "Updating ComfyUI...";
|
||||||
update_comfyui_button.disabled = true;
|
|
||||||
update_comfyui_button.style.backgroundColor = "gray";
|
|
||||||
|
|
||||||
try {
|
set_inprogress_mode();
|
||||||
const response = await api.fetchApi('/comfyui_manager/update_comfyui');
|
|
||||||
|
|
||||||
if (response.status == 400) {
|
const response = await api.fetchApi('/manager/queue/update_comfyui');
|
||||||
show_message('Failed to update ComfyUI.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status == 201) {
|
showTerminal();
|
||||||
show_message('ComfyUI has been successfully updated.');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
show_message('ComfyUI is already up to date with the latest version.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
is_updating = true;
|
||||||
}
|
await api.fetchApi('/manager/queue/start');
|
||||||
catch (exception) {
|
|
||||||
show_message(`Failed to update ComfyUI / ${exception}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
update_comfyui_button.disabled = false;
|
|
||||||
update_comfyui_button.innerText = prev_text;
|
|
||||||
update_comfyui_button.style.backgroundColor = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showVersionSelectorDialog(versions, current, onSelect) {
|
function showVersionSelectorDialog(versions, current, onSelect) {
|
||||||
@@ -648,143 +609,183 @@ function showVersionSelectorDialog(versions, current, onSelect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function switchComfyUI() {
|
async function switchComfyUI() {
|
||||||
let res = await api.fetchApi(`/comfyui_manager/comfyui_versions`, { cache: "no-store" });
|
switch_comfyui_button.disabled = true;
|
||||||
|
switch_comfyui_button.style.backgroundColor = "gray";
|
||||||
|
|
||||||
|
let res = await api.fetchApi(`/comfyui_manager/comfyui_versions`, { cache: "no-store" });
|
||||||
|
|
||||||
if(res.status == 200) {
|
switch_comfyui_button.disabled = false;
|
||||||
let obj = await res.json();
|
switch_comfyui_button.style.backgroundColor = "";
|
||||||
|
|
||||||
let versions = [];
|
if(res.status == 200) {
|
||||||
let default_version;
|
let obj = await res.json();
|
||||||
|
|
||||||
for(let v of obj.versions) {
|
let versions = [];
|
||||||
default_version = v;
|
let default_version;
|
||||||
versions.push(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
showVersionSelectorDialog(versions, obj.current, (selected_version) => {
|
for(let v of obj.versions) {
|
||||||
api.fetchApi(`/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
|
default_version = v;
|
||||||
});
|
versions.push(v);
|
||||||
}
|
|
||||||
else {
|
|
||||||
show_message('Failed to fetch ComfyUI versions.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function fetchUpdates(update_check_checkbox) {
|
|
||||||
let prev_text = fetch_updates_button.innerText;
|
|
||||||
fetch_updates_button.innerText = "Fetching updates...";
|
|
||||||
fetch_updates_button.disabled = true;
|
|
||||||
fetch_updates_button.style.backgroundColor = "gray";
|
|
||||||
|
|
||||||
try {
|
|
||||||
var mode = manager_instance.datasrc_combo.value;
|
|
||||||
|
|
||||||
const response = await api.fetchApi(`/customnode/fetch_updates?mode=${mode}`);
|
|
||||||
|
|
||||||
if (response.status != 200 && response.status != 201) {
|
|
||||||
show_message('Failed to fetch updates.');
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status == 201) {
|
showVersionSelectorDialog(versions, obj.current, async (selected_version) => {
|
||||||
show_message("There is an updated extension available.<BR><BR><P><B>NOTE:<BR>Fetch Updates is not an update.<BR>Please update from <button id='cm-install-customnodes-button'>Install Custom Nodes</button> </B></P>");
|
if(selected_version == 'nightly') {
|
||||||
|
update_policy_combo.value = 'nightly-comfyui';
|
||||||
|
api.fetchApi('/manager/policy/update?value=nightly-comfyui');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
update_policy_combo.value = 'stable-comfyui';
|
||||||
|
api.fetchApi('/manager/policy/update?value=stable-comfyui');
|
||||||
|
}
|
||||||
|
|
||||||
const button = document.getElementById('cm-install-customnodes-button');
|
let response = await api.fetchApi(`/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
|
||||||
button.addEventListener("click",
|
if (response.status == 200) {
|
||||||
async function() {
|
infoToast(`ComfyUI version is switched to ${selected_version}`);
|
||||||
app.ui.dialog.close();
|
}
|
||||||
|
else {
|
||||||
|
customAlert('Failed to switch ComfyUI version.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
customAlert('Failed to fetch ComfyUI versions.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(!CustomNodesManager.instance) {
|
async function onQueueStatus(event) {
|
||||||
CustomNodesManager.instance = new CustomNodesManager(app, self);
|
const isElectron = 'electronAPI' in window;
|
||||||
|
|
||||||
|
if(event.detail.status == 'in_progress') {
|
||||||
|
set_inprogress_mode();
|
||||||
|
update_all_button.innerText = `in progress.. (${event.detail.done_count}/${event.detail.total_count})`;
|
||||||
|
}
|
||||||
|
else if(event.detail.status == 'done') {
|
||||||
|
reset_action_buttons();
|
||||||
|
|
||||||
|
if(!is_updating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_updating = false;
|
||||||
|
|
||||||
|
let success_list = [];
|
||||||
|
let failed_list = [];
|
||||||
|
let comfyui_state = null;
|
||||||
|
|
||||||
|
for(let k in event.detail.nodepack_result){
|
||||||
|
let v = event.detail.nodepack_result[k];
|
||||||
|
|
||||||
|
if(k == 'comfyui') {
|
||||||
|
comfyui_state = v;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(v.msg == 'success') {
|
||||||
|
success_list.push(k);
|
||||||
|
}
|
||||||
|
else if(v.msg != 'skip')
|
||||||
|
failed_list.push(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = "";
|
||||||
|
|
||||||
|
if(success_list.length == 0 && comfyui_state.startsWith('skip')) {
|
||||||
|
if(failed_list.length == 0) {
|
||||||
|
msg += "You are already up to date.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg = "To apply the updates, you need to <button class='cm-small-button' id='cm-reboot-button5'>RESTART</button> ComfyUI.<hr>";
|
||||||
|
|
||||||
|
if(comfyui_state == 'success-nightly') {
|
||||||
|
msg += "ComfyUI has been updated to latest nightly version.<BR><BR>";
|
||||||
|
infoToast("ComfyUI has been updated to the latest nightly version.");
|
||||||
|
}
|
||||||
|
else if(comfyui_state.startsWith('success-stable')) {
|
||||||
|
const ver = comfyui_state.split("-").pop();
|
||||||
|
msg += `ComfyUI has been updated to ${ver}.<BR><BR>`;
|
||||||
|
infoToast(`ComfyUI has been updated to ${ver}`);
|
||||||
|
}
|
||||||
|
else if(comfyui_state == 'skip') {
|
||||||
|
msg += "ComfyUI is already up to date.<BR><BR>"
|
||||||
|
}
|
||||||
|
else if(comfyui_state != null) {
|
||||||
|
msg += "Failed to update ComfyUI.<BR><BR>"
|
||||||
|
}
|
||||||
|
|
||||||
|
if(success_list.length > 0) {
|
||||||
|
msg += "The following custom nodes have been updated:<ul>";
|
||||||
|
for(let x in success_list) {
|
||||||
|
let k = success_list[x];
|
||||||
|
let url = event.detail.nodepack_result[k].url;
|
||||||
|
let title = event.detail.nodepack_result[k].title;
|
||||||
|
if(url) {
|
||||||
|
msg += `<li><a href='${url}' target='_blank'>${title}</a></li>`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg += `<li>${k}</li>`;
|
||||||
}
|
}
|
||||||
await CustomNodesManager.instance.show(CustomNodesManager.ShowMode.UPDATE);
|
|
||||||
}
|
}
|
||||||
);
|
msg += "</ul>";
|
||||||
|
}
|
||||||
|
|
||||||
update_check_checkbox.checked = false;
|
setNeedRestart(true);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
show_message('All extensions are already up-to-date with the latest versions.');
|
if(failed_list.length > 0) {
|
||||||
|
msg += '<br>The update for the following custom nodes has failed:<ul>';
|
||||||
|
for(let x in failed_list) {
|
||||||
|
let k = failed_list[x];
|
||||||
|
let url = event.detail.nodepack_result[k].url;
|
||||||
|
let title = event.detail.nodepack_result[k].title;
|
||||||
|
if(url) {
|
||||||
|
msg += `<li><a href='${url}' target='_blank'>${title}</a></li>`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg += `<li>${k}</li>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg += '</ul>'
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
show_message(msg);
|
||||||
}
|
|
||||||
catch (exception) {
|
const rebootButton = document.getElementById('cm-reboot-button5');
|
||||||
show_message(`Failed to update custom nodes / ${exception}`);
|
rebootButton?.addEventListener("click",
|
||||||
return false;
|
function() {
|
||||||
}
|
if(rebootAPI()) {
|
||||||
finally {
|
manager_dialog.close();
|
||||||
fetch_updates_button.disabled = false;
|
}
|
||||||
fetch_updates_button.innerText = prev_text;
|
});
|
||||||
fetch_updates_button.style.backgroundColor = "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateAll(update_check_checkbox, manager_dialog) {
|
api.addEventListener("cm-queue-status", onQueueStatus);
|
||||||
let prev_text = update_all_button.innerText;
|
|
||||||
update_all_button.innerText = "Updating all...(ComfyUI)";
|
|
||||||
update_all_button.disabled = true;
|
|
||||||
update_all_button.style.backgroundColor = "gray";
|
|
||||||
|
|
||||||
try {
|
|
||||||
var mode = manager_instance.datasrc_combo.value;
|
|
||||||
|
|
||||||
update_all_button.innerText = "Updating all...";
|
async function updateAll(update_comfyui) {
|
||||||
const response1 = await api.fetchApi('/comfyui_manager/update_comfyui');
|
update_all_button.innerText = "Updating...";
|
||||||
const response2 = await api.fetchApi(`/customnode/update_all?mode=${mode}`);
|
|
||||||
|
|
||||||
if (response2.status == 403) {
|
set_inprogress_mode();
|
||||||
show_message('This action is not allowed with this security level configuration.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response1.status == 400 || response2.status == 400) {
|
var mode = manager_instance.datasrc_combo.value;
|
||||||
show_message('Failed to update ComfyUI or several extensions.<BR><BR>See terminal log.<BR>');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(response1.status == 201 || response2.status == 201) {
|
showTerminal();
|
||||||
const update_info = await response2.json();
|
|
||||||
|
|
||||||
let failed_list = "";
|
if(update_comfyui) {
|
||||||
if(update_info.failed.length > 0) {
|
update_all_button.innerText = "Updating ComfyUI...";
|
||||||
failed_list = "<BR>FAILED: "+update_info.failed.join(", ");
|
await api.fetchApi('/manager/queue/update_comfyui');
|
||||||
}
|
|
||||||
|
|
||||||
let updated_list = "";
|
|
||||||
if(update_info.updated.length > 0) {
|
|
||||||
updated_list = "<BR>UPDATED: "+update_info.updated.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
show_message(
|
|
||||||
"ComfyUI and all extensions have been updated to the latest version.<BR>To apply the updated custom node, please <button class='cm-small-button' id='cm-reboot-button5'>RESTART</button> ComfyUI. And refresh browser.<BR>"
|
|
||||||
+failed_list
|
|
||||||
+updated_list
|
|
||||||
);
|
|
||||||
|
|
||||||
const rebootButton = document.getElementById('cm-reboot-button5');
|
|
||||||
rebootButton.addEventListener("click",
|
|
||||||
function() {
|
|
||||||
if(rebootAPI()) {
|
|
||||||
manager_dialog.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
show_message('ComfyUI and all extensions are already up-to-date with the latest versions.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
catch (exception) {
|
|
||||||
show_message(`Failed to update ComfyUI or several extensions / ${exception}`);
|
const response = await api.fetchApi(`/manager/queue/update_all?mode=${mode}`);
|
||||||
return false;
|
|
||||||
|
if (response.status == 401) {
|
||||||
|
customAlert('Another task is already in progress. Please stop the ongoing task first.');
|
||||||
}
|
}
|
||||||
finally {
|
else if(response.status == 200) {
|
||||||
update_all_button.disabled = false;
|
is_updating = true;
|
||||||
update_all_button.innerText = prev_text;
|
await api.fetchApi('/manager/queue/start');
|
||||||
update_all_button.style.backgroundColor = "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,15 +809,29 @@ const isOutputNode = (node) => {
|
|||||||
return SUPPORTED_OUTPUT_NODE_TYPES.includes(node.type);
|
return SUPPORTED_OUTPUT_NODE_TYPES.includes(node.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restartOrStop() {
|
||||||
|
if(restart_stop_button.innerText == 'Restart'){
|
||||||
|
rebootAPI();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
api.fetchApi('/manager/queue/reset');
|
||||||
|
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -----------
|
// -----------
|
||||||
class ManagerMenuDialog extends ComfyDialog {
|
class ManagerMenuDialog extends ComfyDialog {
|
||||||
createControlsMid() {
|
createControlsMid() {
|
||||||
let self = this;
|
let self = this;
|
||||||
|
const isElectron = 'electronAPI' in window;
|
||||||
|
|
||||||
update_comfyui_button =
|
update_comfyui_button =
|
||||||
$el("button.cm-button", {
|
$el("button.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Update ComfyUI",
|
textContent: "Update ComfyUI",
|
||||||
|
style: {
|
||||||
|
display: isElectron ? 'none' : 'block'
|
||||||
|
},
|
||||||
onclick:
|
onclick:
|
||||||
() => updateComfyUI()
|
() => updateComfyUI()
|
||||||
});
|
});
|
||||||
@@ -825,25 +840,38 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
$el("button.cm-button", {
|
$el("button.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Switch ComfyUI",
|
textContent: "Switch ComfyUI",
|
||||||
|
style: {
|
||||||
|
display: isElectron ? 'none' : 'block'
|
||||||
|
},
|
||||||
onclick:
|
onclick:
|
||||||
() => switchComfyUI()
|
() => switchComfyUI()
|
||||||
});
|
});
|
||||||
|
|
||||||
fetch_updates_button =
|
restart_stop_button =
|
||||||
$el("button.cm-button", {
|
$el("button.cm-button-red", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Fetch Updates",
|
textContent: "Restart",
|
||||||
onclick:
|
onclick: () => restartOrStop()
|
||||||
() => fetchUpdates(this.update_check_checkbox)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
update_all_button =
|
if(isElectron) {
|
||||||
$el("button.cm-button", {
|
update_all_button =
|
||||||
type: "button",
|
$el("button.cm-button", {
|
||||||
textContent: "Update All",
|
type: "button",
|
||||||
onclick:
|
textContent: "Update All Custom Nodes",
|
||||||
() => updateAll(this.update_check_checkbox, self)
|
onclick:
|
||||||
});
|
() => updateAll(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
update_all_button =
|
||||||
|
$el("button.cm-button", {
|
||||||
|
type: "button",
|
||||||
|
textContent: "Update All",
|
||||||
|
onclick:
|
||||||
|
() => updateAll(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const res =
|
const res =
|
||||||
[
|
[
|
||||||
@@ -871,7 +899,19 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
$el("button.cm-button", {
|
||||||
|
type: "button",
|
||||||
|
textContent: "Custom Nodes In Workflow",
|
||||||
|
onclick:
|
||||||
|
() => {
|
||||||
|
if(!CustomNodesManager.instance) {
|
||||||
|
CustomNodesManager.instance = new CustomNodesManager(app, self);
|
||||||
|
}
|
||||||
|
CustomNodesManager.instance.show(CustomNodesManager.ShowMode.IN_WORKFLOW);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
$el("br", {}, []),
|
||||||
$el("button.cm-button", {
|
$el("button.cm-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Model Manager",
|
textContent: "Model Manager",
|
||||||
@@ -900,62 +940,19 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
update_all_button,
|
update_all_button,
|
||||||
update_comfyui_button,
|
update_comfyui_button,
|
||||||
switch_comfyui_button,
|
switch_comfyui_button,
|
||||||
fetch_updates_button,
|
// fetch_updates_button,
|
||||||
|
|
||||||
$el("br", {}, []),
|
$el("br", {}, []),
|
||||||
$el("button.cm-button", {
|
restart_stop_button,
|
||||||
type: "button",
|
|
||||||
textContent: "Alternatives of A1111",
|
|
||||||
onclick:
|
|
||||||
() => {
|
|
||||||
if(!CustomNodesManager.instance) {
|
|
||||||
CustomNodesManager.instance = new CustomNodesManager(app, self);
|
|
||||||
}
|
|
||||||
CustomNodesManager.instance.show(CustomNodesManager.ShowMode.ALTERNATIVES);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
$el("br", {}, []),
|
|
||||||
$el("button.cm-button-red", {
|
|
||||||
type: "button",
|
|
||||||
textContent: "Restart",
|
|
||||||
onclick: () => rebootAPI()
|
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let migration_btn =
|
|
||||||
$el("button.cm-button-orange", {
|
|
||||||
type: "button",
|
|
||||||
textContent: "Migrate to New Node System",
|
|
||||||
onclick: () => migrateAPI()
|
|
||||||
});
|
|
||||||
|
|
||||||
migration_btn.style.display = 'none';
|
|
||||||
|
|
||||||
res.push(migration_btn);
|
|
||||||
|
|
||||||
api.fetchApi('/manager/need_to_migrate')
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(text => {
|
|
||||||
if (text === 'True') {
|
|
||||||
migration_btn.style.display = 'block';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error checking migration status:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
createControlsLeft() {
|
createControlsLeft() {
|
||||||
let self = this;
|
const isElectron = 'electronAPI' in window;
|
||||||
|
|
||||||
this.update_check_checkbox = $el("input",{type:'checkbox', id:"skip_update_check"},[])
|
let self = this;
|
||||||
const uc_checkbox_text = $el("label",{for:"skip_update_check"},[" Skip update check"])
|
|
||||||
uc_checkbox_text.style.color = "var(--fg-color)";
|
|
||||||
uc_checkbox_text.style.cursor = "pointer";
|
|
||||||
this.update_check_checkbox.checked = true;
|
|
||||||
|
|
||||||
// db mode
|
// db mode
|
||||||
this.datasrc_combo = document.createElement("select");
|
this.datasrc_combo = document.createElement("select");
|
||||||
@@ -965,6 +962,14 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, []));
|
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'DB: Local' }, []));
|
||||||
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'DB: Channel (remote)' }, []));
|
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'DB: Channel (remote)' }, []));
|
||||||
|
|
||||||
|
api.fetchApi('/manager/db_mode')
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => { this.datasrc_combo.value = data; });
|
||||||
|
|
||||||
|
this.datasrc_combo.addEventListener('change', function (event) {
|
||||||
|
api.fetchApi(`/manager/db_mode?value=${event.target.value}`);
|
||||||
|
});
|
||||||
|
|
||||||
// preview method
|
// preview method
|
||||||
let preview_combo = document.createElement("select");
|
let preview_combo = document.createElement("select");
|
||||||
preview_combo.setAttribute("title", "Configure how latent variables will be decoded during preview in the sampling process.");
|
preview_combo.setAttribute("title", "Configure how latent variables will be decoded during preview in the sampling process.");
|
||||||
@@ -1009,21 +1014,6 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// default ui state
|
|
||||||
let default_ui_combo = document.createElement("select");
|
|
||||||
default_ui_combo.setAttribute("title", "Set the default state to be displayed in the main menu when the browser starts.");
|
|
||||||
default_ui_combo.className = "cm-menu-combo";
|
|
||||||
default_ui_combo.appendChild($el('option', { value: 'none', text: 'Default UI: None' }, []));
|
|
||||||
default_ui_combo.appendChild($el('option', { value: 'history', text: 'Default UI: History' }, []));
|
|
||||||
default_ui_combo.appendChild($el('option', { value: 'queue', text: 'Default UI: Queue' }, []));
|
|
||||||
api.fetchApi('/manager/default_ui')
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(data => { default_ui_combo.value = data; });
|
|
||||||
|
|
||||||
default_ui_combo.addEventListener('change', function (event) {
|
|
||||||
api.fetchApi(`/manager/default_ui?value=${event.target.value}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// share
|
// share
|
||||||
let share_combo = document.createElement("select");
|
let share_combo = document.createElement("select");
|
||||||
@@ -1042,47 +1032,6 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
|
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
|
||||||
}
|
}
|
||||||
|
|
||||||
// default ui state
|
|
||||||
let component_policy_combo = document.createElement("select");
|
|
||||||
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use.");
|
|
||||||
component_policy_combo.className = "cm-menu-combo";
|
|
||||||
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Component: Use workflow version' }, []));
|
|
||||||
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Component: Use higher version' }, []));
|
|
||||||
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Component: Use my version' }, []));
|
|
||||||
api.fetchApi('/manager/component/policy')
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(data => {
|
|
||||||
component_policy_combo.value = data;
|
|
||||||
set_component_policy(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
component_policy_combo.addEventListener('change', function (event) {
|
|
||||||
api.fetchApi(`/manager/component/policy?value=${event.target.value}`);
|
|
||||||
set_component_policy(event.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
let dbl_click_policy_combo = document.createElement("select");
|
|
||||||
dbl_click_policy_combo.setAttribute("title", "Sets the behavior when you double-click the title area of a node.");
|
|
||||||
dbl_click_policy_combo.className = "cm-menu-combo";
|
|
||||||
dbl_click_policy_combo.appendChild($el('option', { value: 'none', text: 'Double-Click: None' }, []));
|
|
||||||
dbl_click_policy_combo.appendChild($el('option', { value: 'copy-all', text: 'Double-Click: Copy All Connections' }, []));
|
|
||||||
dbl_click_policy_combo.appendChild($el('option', { value: 'copy-full', text: 'Double-Click: Copy All Connections and shape' }, []));
|
|
||||||
dbl_click_policy_combo.appendChild($el('option', { value: 'copy-input', text: 'Double-Click: Copy Input Connections' }, []));
|
|
||||||
dbl_click_policy_combo.appendChild($el('option', { value: 'possible-input', text: 'Double-Click: Possible Input Connections' }, []));
|
|
||||||
dbl_click_policy_combo.appendChild($el('option', { value: 'dual', text: 'Double-Click: Possible(left) + Copy(right)' }, []));
|
|
||||||
|
|
||||||
api.fetchApi('/manager/dbl_click/policy')
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(data => {
|
|
||||||
dbl_click_policy_combo.value = data;
|
|
||||||
set_double_click_policy(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
dbl_click_policy_combo.addEventListener('change', function (event) {
|
|
||||||
api.fetchApi(`/manager/dbl_click/policy?value=${event.target.value}`);
|
|
||||||
set_double_click_policy(event.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
api.fetchApi('/manager/share_option')
|
api.fetchApi('/manager/share_option')
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -1102,16 +1051,51 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let component_policy_combo = document.createElement("select");
|
||||||
|
component_policy_combo.setAttribute("title", "When loading the workflow, configure which version of the component to use.");
|
||||||
|
component_policy_combo.className = "cm-menu-combo";
|
||||||
|
component_policy_combo.appendChild($el('option', { value: 'workflow', text: 'Component: Use workflow version' }, []));
|
||||||
|
component_policy_combo.appendChild($el('option', { value: 'higher', text: 'Component: Use higher version' }, []));
|
||||||
|
component_policy_combo.appendChild($el('option', { value: 'mine', text: 'Component: Use my version' }, []));
|
||||||
|
api.fetchApi('/manager/policy/component')
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
component_policy_combo.value = data;
|
||||||
|
set_component_policy(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
component_policy_combo.addEventListener('change', function (event) {
|
||||||
|
api.fetchApi(`/manager/policy/component?value=${event.target.value}`);
|
||||||
|
set_component_policy(event.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
update_policy_combo = document.createElement("select");
|
||||||
|
|
||||||
|
if(isElectron)
|
||||||
|
update_policy_combo.style.display = 'none';
|
||||||
|
|
||||||
|
update_policy_combo.setAttribute("title", "Sets the policy to be applied when performing an update.");
|
||||||
|
update_policy_combo.className = "cm-menu-combo";
|
||||||
|
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'Update: ComfyUI Stable Version' }, []));
|
||||||
|
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'Update: ComfyUI Nightly Version' }, []));
|
||||||
|
api.fetchApi('/manager/policy/update')
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(data => {
|
||||||
|
update_policy_combo.value = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
update_policy_combo.addEventListener('change', function (event) {
|
||||||
|
api.fetchApi(`/manager/policy/update?value=${event.target.value}`);
|
||||||
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
$el("div", {}, [this.update_check_checkbox, uc_checkbox_text]),
|
|
||||||
$el("br", {}, []),
|
$el("br", {}, []),
|
||||||
this.datasrc_combo,
|
this.datasrc_combo,
|
||||||
channel_combo,
|
channel_combo,
|
||||||
preview_combo,
|
preview_combo,
|
||||||
default_ui_combo,
|
|
||||||
share_combo,
|
share_combo,
|
||||||
component_policy_combo,
|
component_policy_combo,
|
||||||
dbl_click_policy_combo,
|
update_policy_combo,
|
||||||
$el("br", {}, []),
|
$el("br", {}, []),
|
||||||
|
|
||||||
$el("br", {}, []),
|
$el("br", {}, []),
|
||||||
@@ -1138,11 +1122,6 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
install_pip(url, self);
|
install_pip(url, self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
$el("button.cm-experimental-button", {
|
|
||||||
type: "button",
|
|
||||||
textContent: "Unload models",
|
|
||||||
onclick: () => { free_models(); }
|
|
||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
@@ -1271,7 +1250,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
$el("div.comfy-modal-content",
|
$el("div.comfy-modal-content",
|
||||||
[
|
[
|
||||||
$el("tr.cm-title", {}, [
|
$el("tr.cm-title", {}, [
|
||||||
$el("font", {size:6, color:"white"}, [`ComfyUI Manager Menu`])]
|
$el("font", {size:6, color:"white"}, [`ComfyUI Manager ${manager_version}`])]
|
||||||
),
|
),
|
||||||
$el("br", {}, []),
|
$el("br", {}, []),
|
||||||
$el("div.cm-menu-container",
|
$el("div.cm-menu-container",
|
||||||
@@ -1292,10 +1271,22 @@ class ManagerMenuDialog extends ComfyDialog {
|
|||||||
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
|
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isVisible() {
|
||||||
|
return this.element?.style?.display !== "none";
|
||||||
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
this.element.style.display = "block";
|
this.element.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleVisibility() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.close();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleWorkflowGalleryButtonClick(e) {
|
handleWorkflowGalleryButtonClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -1401,18 +1392,52 @@ async function getVersion() {
|
|||||||
return await version.text();
|
return await version.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.ManagerMenu",
|
name: "Comfy.ManagerMenu",
|
||||||
|
|
||||||
aboutPageBadges: [
|
aboutPageBadges: [
|
||||||
{
|
{
|
||||||
label: `ComfyUI-Manager ${await getVersion()}`,
|
label: `ComfyUI-Manager ${manager_version}`,
|
||||||
url: 'https://github.com/ltdrdata/ComfyUI-Manager',
|
url: 'https://github.com/ltdrdata/ComfyUI-Manager',
|
||||||
icon: 'pi pi-th-large'
|
icon: 'pi pi-th-large'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
id: "Comfy.Manager.Menu.ToggleVisibility",
|
||||||
|
label: "Toggle Manager Menu Visibility",
|
||||||
|
icon: "mdi mdi-puzzle",
|
||||||
|
function: () => {
|
||||||
|
if (!manager_instance) {
|
||||||
|
setManagerInstance(new ManagerMenuDialog());
|
||||||
|
manager_instance.show();
|
||||||
|
} else {
|
||||||
|
manager_instance.toggleVisibility();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "Comfy.Manager.CustomNodesManager.ToggleVisibility",
|
||||||
|
label: "Toggle Custom Nodes Manager Visibility",
|
||||||
|
icon: "pi pi-server",
|
||||||
|
function: () => {
|
||||||
|
if (CustomNodesManager.instance?.isVisible) {
|
||||||
|
CustomNodesManager.instance.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manager_instance) {
|
||||||
|
setManagerInstance(new ManagerMenuDialog());
|
||||||
|
}
|
||||||
|
if (!CustomNodesManager.instance) {
|
||||||
|
CustomNodesManager.instance = new CustomNodesManager(app, self);
|
||||||
|
}
|
||||||
|
CustomNodesManager.instance.show(CustomNodesManager.ShowMode.NORMAL);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
$el("style", {
|
$el("style", {
|
||||||
textContent: style,
|
textContent: style,
|
||||||
@@ -1607,27 +1632,3 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
async function set_default_ui()
|
|
||||||
{
|
|
||||||
let res = await api.fetchApi('/manager/default_ui');
|
|
||||||
if(res.status == 200) {
|
|
||||||
let mode = await res.text();
|
|
||||||
switch(mode) {
|
|
||||||
case 'history':
|
|
||||||
app.ui.queue.hide();
|
|
||||||
app.ui.history.show();
|
|
||||||
break;
|
|
||||||
case 'queue':
|
|
||||||
app.ui.queue.show();
|
|
||||||
app.ui.history.hide();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// do nothing
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set_default_ui();
|
|
||||||
293
js/common.js
293
js/common.js
@@ -1,6 +1,7 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { $el, ComfyDialog } from "../../scripts/ui.js";
|
import { $el, ComfyDialog } from "../../scripts/ui.js";
|
||||||
|
import { getBestPosition, getPositionStyle, getRect } from './popover-helper.js';
|
||||||
|
|
||||||
|
|
||||||
function internalCustomConfirm(message, confirmMessage, cancelMessage) {
|
function internalCustomConfirm(message, confirmMessage, cancelMessage) {
|
||||||
@@ -130,6 +131,20 @@ export function customAlert(message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function infoToast(summary, message) {
|
||||||
|
try {
|
||||||
|
app.extensionManager.toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: summary,
|
||||||
|
detail: message,
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function customPrompt(title, message) {
|
export async function customPrompt(title, message) {
|
||||||
try {
|
try {
|
||||||
@@ -167,23 +182,6 @@ export function rebootAPI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function migrateAPI() {
|
|
||||||
let confirmed = await customConfirm("When performing a migration, existing installed custom nodes will be renamed and the server will be restarted. Are you sure you want to apply this?\n\n(If you don't perform the migration, ComfyUI-Manager's start-up time will be longer each time due to re-checking during startup.)")
|
|
||||||
if (confirmed) {
|
|
||||||
try {
|
|
||||||
await api.fetchApi("/manager/migrate_unmanaged_nodes");
|
|
||||||
api.fetchApi("/manager/reboot");
|
|
||||||
}
|
|
||||||
catch(exception) {
|
|
||||||
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export var manager_instance = null;
|
export var manager_instance = null;
|
||||||
|
|
||||||
export function setManagerInstance(obj) {
|
export function setManagerInstance(obj) {
|
||||||
@@ -390,10 +388,267 @@ export async function fetchData(route, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://cenfun.github.io/open-icons/
|
||||||
export const icons = {
|
export const icons = {
|
||||||
search: '<svg viewBox="0 0 24 24" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.486-4.494M19 10.5a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0"/></svg>',
|
search: '<svg viewBox="0 0 24 24" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.486-4.494M19 10.5a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0"/></svg>',
|
||||||
extensions: '<svg viewBox="64 64 896 896" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M843.5 737.4c-12.4-75.2-79.2-129.1-155.3-125.4S550.9 676 546 752c-153.5-4.8-208-40.7-199.1-113.7 3.3-27.3 19.8-41.9 50.1-49 18.4-4.3 38.8-4.9 57.3-3.2 1.7.2 3.5.3 5.2.5 11.3 2.7 22.8 5 34.3 6.8 34.1 5.6 68.8 8.4 101.8 6.6 92.8-5 156-45.9 159.2-132.7 3.1-84.1-54.7-143.7-147.9-183.6-29.9-12.8-61.6-22.7-93.3-30.2-14.3-3.4-26.3-5.7-35.2-7.2-7.9-75.9-71.5-133.8-147.8-134.4S189.7 168 180.5 243.8s40 146.3 114.2 163.9 149.9-23.3 175.7-95.1c9.4 1.7 18.7 3.6 28 5.8 28.2 6.6 56.4 15.4 82.4 26.6 70.7 30.2 109.3 70.1 107.5 119.9-1.6 44.6-33.6 65.2-96.2 68.6-27.5 1.5-57.6-.9-87.3-5.8-8.3-1.4-15.9-2.8-22.6-4.3-3.9-.8-6.6-1.5-7.8-1.8l-3.1-.6c-2.2-.3-5.9-.8-10.7-1.3-25-2.3-52.1-1.5-78.5 4.6-55.2 12.9-93.9 47.2-101.1 105.8-15.7 126.2 78.6 184.7 276 188.9 29.1 70.4 106.4 107.9 179.6 87 73.3-20.9 119.3-93.4 106.9-168.6M329.1 345.2a83.3 83.3 0 1 1 .01-166.61 83.3 83.3 0 0 1-.01 166.61M695.6 845a83.3 83.3 0 1 1 .01-166.61A83.3 83.3 0 0 1 695.6 845"/></svg>',
|
|
||||||
conflicts: '<svg viewBox="0 0 400 400" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m397.2 350.4.2-.2-180-320-.2.2C213.8 24.2 207.4 20 200 20s-13.8 4.2-17.2 10.4l-.2-.2-180 320 .2.2c-1.6 2.8-2.8 6-2.8 9.6 0 11 9 20 20 20h360c11 0 20-9 20-20 0-3.6-1.2-6.8-2.8-9.6M220 340h-40v-40h40zm0-60h-40V120h40z"/></svg>',
|
conflicts: '<svg viewBox="0 0 400 400" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m397.2 350.4.2-.2-180-320-.2.2C213.8 24.2 207.4 20 200 20s-13.8 4.2-17.2 10.4l-.2-.2-180 320 .2.2c-1.6 2.8-2.8 6-2.8 9.6 0 11 9 20 20 20h360c11 0 20-9 20-20 0-3.6-1.2-6.8-2.8-9.6M220 340h-40v-40h40zm0-60h-40V120h40z"/></svg>',
|
||||||
passed: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.667 426.667"><path fill="#6AC259" d="M213.333,0C95.518,0,0,95.514,0,213.333s95.518,213.333,213.333,213.333c117.828,0,213.333-95.514,213.333-213.333S331.157,0,213.333,0z M174.199,322.918l-93.935-93.931l31.309-31.309l62.626,62.622l140.894-140.898l31.309,31.309L174.199,322.918z"/></svg>',
|
passed: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.667 426.667"><path fill="#6AC259" d="M213.333,0C95.518,0,0,95.514,0,213.333s95.518,213.333,213.333,213.333c117.828,0,213.333-95.514,213.333-213.333S331.157,0,213.333,0z M174.199,322.918l-93.935-93.931l31.309-31.309l62.626,62.622l140.894-140.898l31.309,31.309L174.199,322.918z"/></svg>',
|
||||||
download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" width="100%" height="100%" viewBox="0 0 32 32"><path fill="currentColor" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10l10-10z"></path></svg>'
|
download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" width="100%" height="100%" viewBox="0 0 32 32"><path fill="currentColor" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10l10-10z"></path></svg>',
|
||||||
|
close: '<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="100%" height="100%" viewBox="0 0 16 16"><g fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="m7.116 8-4.558 4.558.884.884L8 8.884l4.558 4.558.884-.884L8.884 8l4.558-4.558-.884-.884L8 7.116 3.442 2.558l-.884.884L7.116 8z"/></g></svg>',
|
||||||
|
arrowRight: '<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="100%" height="100%" viewBox="0 0 20 20"><path fill="currentColor" fill-rule="evenodd" d="m2.542 2.154 7.254 7.26c.136.14.204.302.204.483a.73.73 0 0 1-.204.5l-7.575 7.398c-.383.317-.724.317-1.022 0-.299-.317-.299-.643 0-.98l7.08-6.918-6.754-6.763c-.237-.343-.215-.654.066-.935.281-.28.598-.295.951-.045Zm9 0 7.254 7.26c.136.14.204.302.204.483a.73.73 0 0 1-.204.5l-7.575 7.398c-.383.317-.724.317-1.022 0-.299-.317-.299-.643 0-.98l7.08-6.918-6.754-6.763c-.237-.343-.215-.654.066-.935.281-.28.598-.295.951-.045Z"/></svg>'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeHTML(str) {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showTerminal() {
|
||||||
|
try {
|
||||||
|
const panel = app.extensionManager.bottomPanel;
|
||||||
|
const isTerminalVisible = panel.bottomPanelVisible && panel.activeBottomPanelTab.id === 'logs-terminal';
|
||||||
|
if (!isTerminalVisible)
|
||||||
|
panel.toggleBottomPanelTab('logs-terminal');
|
||||||
|
}
|
||||||
|
catch(exception) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let need_restart = false;
|
||||||
|
|
||||||
|
export function setNeedRestart(value) {
|
||||||
|
need_restart = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onReconnected(event) {
|
||||||
|
if(need_restart) {
|
||||||
|
setNeedRestart(false);
|
||||||
|
|
||||||
|
const confirmed = await customConfirm("To apply the changes to the node pack's installation status, you need to refresh the browser. Would you like to refresh?");
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.addEventListener('reconnected', onReconnected);
|
||||||
|
|
||||||
|
const storeId = "comfyui-manager-grid";
|
||||||
|
let timeId;
|
||||||
|
export function storeColumnWidth(gridId, columnItem) {
|
||||||
|
clearTimeout(timeId);
|
||||||
|
timeId = setTimeout(() => {
|
||||||
|
let data = {};
|
||||||
|
const dataStr = localStorage.getItem(storeId);
|
||||||
|
if (dataStr) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(dataStr);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data[gridId]) {
|
||||||
|
data[gridId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
data[gridId][columnItem.id] = columnItem.width;
|
||||||
|
|
||||||
|
localStorage.setItem(storeId, JSON.stringify(data));
|
||||||
|
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreColumnWidth(gridId, columns) {
|
||||||
|
const dataStr = localStorage.getItem(storeId);
|
||||||
|
if (!dataStr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(dataStr);
|
||||||
|
} catch (e) {}
|
||||||
|
if(!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const widthMap = data[gridId];
|
||||||
|
if (!widthMap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.forEach(columnItem => {
|
||||||
|
const w = widthMap[columnItem.id];
|
||||||
|
if (w) {
|
||||||
|
columnItem.width = w;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimeAgo(dateStr) {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
|
||||||
|
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const units = [
|
||||||
|
{ max: 2760000, value: 60000, name: 'minute', past: 'a minute ago', future: 'in a minute' },
|
||||||
|
{ max: 72000000, value: 3600000, name: 'hour', past: 'an hour ago', future: 'in an hour' },
|
||||||
|
{ max: 518400000, value: 86400000, name: 'day', past: 'yesterday', future: 'tomorrow' },
|
||||||
|
{ max: 2419200000, value: 604800000, name: 'week', past: 'last week', future: 'in a week' },
|
||||||
|
{ max: 28512000000, value: 2592000000, name: 'month', past: 'last month', future: 'in a month' }
|
||||||
|
];
|
||||||
|
const diff = Date.now() - date.getTime();
|
||||||
|
// less than a minute
|
||||||
|
if (Math.abs(diff) < 60000)
|
||||||
|
return 'just now';
|
||||||
|
for (let i = 0; i < units.length; i++) {
|
||||||
|
if (Math.abs(diff) < units[i].max) {
|
||||||
|
return format(diff, units[i].value, units[i].name, units[i].past, units[i].future, diff < 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function format(diff, divisor, unit, past, future, isInTheFuture) {
|
||||||
|
const val = Math.round(Math.abs(diff) / divisor);
|
||||||
|
if (isInTheFuture)
|
||||||
|
return val <= 1 ? future : 'in ' + val + ' ' + unit + 's';
|
||||||
|
return val <= 1 ? past : val + ' ' + unit + 's ago';
|
||||||
|
}
|
||||||
|
return format(diff, 31536000000, 'year', 'last year', 'in a year', diff < 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadCss = (cssFile) => {
|
||||||
|
const cssPath = import.meta.resolve(cssFile);
|
||||||
|
//console.log(cssPath);
|
||||||
|
const $link = document.createElement("link");
|
||||||
|
$link.setAttribute("rel", 'stylesheet');
|
||||||
|
$link.setAttribute("href", cssPath);
|
||||||
|
document.head.appendChild($link);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copyText = (text) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
resolve(false);
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderPopover($elem, target, options = {}) {
|
||||||
|
// async microtask
|
||||||
|
queueMicrotask(() => {
|
||||||
|
|
||||||
|
const containerRect = getRect(window);
|
||||||
|
const targetRect = getRect(target);
|
||||||
|
const elemRect = getRect($elem);
|
||||||
|
|
||||||
|
const positionInfo = getBestPosition(
|
||||||
|
containerRect,
|
||||||
|
targetRect,
|
||||||
|
elemRect,
|
||||||
|
options.positions
|
||||||
|
);
|
||||||
|
const style = getPositionStyle(positionInfo, {
|
||||||
|
bgColor: options.bgColor,
|
||||||
|
borderColor: options.borderColor,
|
||||||
|
borderRadius: options.borderRadius
|
||||||
|
});
|
||||||
|
|
||||||
|
$elem.style.top = positionInfo.top + "px";
|
||||||
|
$elem.style.left = positionInfo.left + "px";
|
||||||
|
$elem.style.background = style.background;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let $popover;
|
||||||
|
export function hidePopover() {
|
||||||
|
if ($popover) {
|
||||||
|
$popover.remove();
|
||||||
|
$popover = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function showPopover(target, text, className, options) {
|
||||||
|
hidePopover();
|
||||||
|
$popover = document.createElement("div");
|
||||||
|
$popover.className = ['cn-popover', className].filter(it => it).join(" ");
|
||||||
|
document.body.appendChild($popover);
|
||||||
|
$popover.innerHTML = text;
|
||||||
|
$popover.style.display = "block";
|
||||||
|
renderPopover($popover, target, {
|
||||||
|
borderRadius: 10,
|
||||||
|
... options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let $tooltip;
|
||||||
|
export function hideTooltip(target) {
|
||||||
|
if ($tooltip) {
|
||||||
|
$tooltip.style.display = "none";
|
||||||
|
$tooltip.innerHTML = "";
|
||||||
|
$tooltip.style.top = "0px";
|
||||||
|
$tooltip.style.left = "0px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {}) {
|
||||||
|
if (!$tooltip) {
|
||||||
|
$tooltip = document.createElement("div");
|
||||||
|
$tooltip.className = className;
|
||||||
|
$tooltip.style.cssText = `
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10001;
|
||||||
|
padding: 20px;
|
||||||
|
color: #1e1e1e;
|
||||||
|
max-width: 350px;
|
||||||
|
filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%));
|
||||||
|
${Object.keys(styleMap).map(k=>k+":"+styleMap[k]+";").join("")}
|
||||||
|
`;
|
||||||
|
document.body.appendChild($tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tooltip.innerHTML = text;
|
||||||
|
$tooltip.style.display = "block";
|
||||||
|
renderPopover($tooltip, target, {
|
||||||
|
positions: ['top', 'bottom', 'right', 'center'],
|
||||||
|
bgColor: "#ffffff",
|
||||||
|
borderColor: "#cccccc",
|
||||||
|
borderRadius: 5
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTooltip () {
|
||||||
|
const mouseenterHandler = (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
const text = target.getAttribute('tooltip');
|
||||||
|
if (text) {
|
||||||
|
showTooltip(target, text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mouseleaveHandler = (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
const text = target.getAttribute('tooltip');
|
||||||
|
if (text) {
|
||||||
|
hideTooltip(target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.body.removeEventListener('mouseenter', mouseenterHandler, true);
|
||||||
|
document.body.removeEventListener('mouseleave', mouseleaveHandler, true);
|
||||||
|
document.body.addEventListener('mouseenter', mouseenterHandler, true);
|
||||||
|
document.body.addEventListener('mouseleave', mouseleaveHandler, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
initTooltip();
|
||||||
@@ -709,7 +709,7 @@ app.handleFile = handleFile;
|
|||||||
|
|
||||||
let current_component_policy = 'workflow';
|
let current_component_policy = 'workflow';
|
||||||
try {
|
try {
|
||||||
api.fetchApi('/manager/component/policy')
|
api.fetchApi('/manager/policy/component')
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => { current_component_policy = data; });
|
.then(data => { current_component_policy = data; });
|
||||||
}
|
}
|
||||||
|
|||||||
699
js/custom-nodes-manager.css
Normal file
699
js/custom-nodes-manager.css
Normal file
@@ -0,0 +1,699 @@
|
|||||||
|
.cn-manager {
|
||||||
|
--grid-font: -apple-system, BlinkMacSystemFont, "Segue UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||||
|
z-index: 1099;
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--fg-color);
|
||||||
|
font-family: arial, sans-serif;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-flex-auto {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager button {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--input-text);
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-color: var(--border-color);
|
||||||
|
border-style: solid;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager button:disabled,
|
||||||
|
.cn-manager input:disabled,
|
||||||
|
.cn-manager select:disabled {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager button:disabled {
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-manager-restart {
|
||||||
|
display: none;
|
||||||
|
background-color: #500000;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-manager-stop {
|
||||||
|
display: none;
|
||||||
|
background-color: #500000;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-manager-back {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
margin-right: 5px;
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-icon {
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-icon svg {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-header label {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-filter {
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-keywords {
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
padding: 0 5px 0 26px;
|
||||||
|
background-size: 16px;
|
||||||
|
background-position: 5px center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-status {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid {
|
||||||
|
flex: auto;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-selection {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-message {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid .tg-turbogrid {
|
||||||
|
font-family: var(--grid-font);
|
||||||
|
font-size: 15px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid .tg-turbogrid .tg-highlight::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #80bdff11;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid .cn-pack-name a {
|
||||||
|
color: skyblue;
|
||||||
|
text-decoration: none;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid .cn-pack-desc a {
|
||||||
|
color: #5555FF;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid .tg-cell a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid .cn-pack-version {
|
||||||
|
line-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid .cn-pack-nodes {
|
||||||
|
line-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid .cn-pack-nodes:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-grid .cn-pack-conflicts {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-popover {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
padding: 20px;
|
||||||
|
color: #1e1e1e;
|
||||||
|
filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%));
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-flyover {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--comfy-menu-bg);
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-flyover::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
content: "";
|
||||||
|
z-index: 10;
|
||||||
|
display: block;
|
||||||
|
width: 10px;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
left: -10px;
|
||||||
|
background-image: linear-gradient(to left, rgb(0 0 0 / 20%), rgb(0 0 0 / 0%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-flyover-header {
|
||||||
|
height: 45px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-flyover-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.8;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-flyover-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-flyover-close svg {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-flyover-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: bold;
|
||||||
|
gap: 10px;
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-flyover-body {
|
||||||
|
height: calc(100% - 45px);
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--comfy-menu-secondary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cn-slide-in-right {
|
||||||
|
from {
|
||||||
|
visibility: visible;
|
||||||
|
transform: translate3d(100%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-slide-in-right {
|
||||||
|
animation-name: cn-slide-in-right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cn-slide-out-right {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translate3d(100%, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-slide-out-right {
|
||||||
|
animation-name: cn-slide-out-right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-list {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-row:nth-child(odd) {
|
||||||
|
background-color: rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-row:hover {
|
||||||
|
background-color: rgb(0 0 0 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-sn {
|
||||||
|
text-align: right;
|
||||||
|
min-width: 35px;
|
||||||
|
color: var(--drag-text);
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-name {
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-name::after {
|
||||||
|
content: attr(action);
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 50%;
|
||||||
|
left: 100%;
|
||||||
|
transform: translate(5px, -50%);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--drag-text);
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 3px 8px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-name.action::after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-name:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-conflict .cn-nodes-name,
|
||||||
|
.cn-nodes-conflict .cn-icon {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-conflicts-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-conflicts-list b {
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--descrip-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-pack {
|
||||||
|
cursor: pointer;
|
||||||
|
color: skyblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-nodes-pack:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-pack-badge {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 3px 8px;
|
||||||
|
color: var(--error-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview {
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 500px;
|
||||||
|
min-height: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 12px;
|
||||||
|
color: var(--fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--comfy-input-bg);
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: grey;
|
||||||
|
position: relative;
|
||||||
|
filter: drop-shadow(1px 2px 3px rgb(0 0 0 / 30%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-dot.cn-preview-optional::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-dot.cn-preview-grid {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-dot.cn-preview-grid::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
border-left: 1px solid var(--comfy-input-bg);
|
||||||
|
border-right: 1px solid var(--comfy-input-bg);
|
||||||
|
width: 4px;
|
||||||
|
height: 100%;
|
||||||
|
left: 2px;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-dot.cn-preview-grid::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
border-top: 1px solid var(--comfy-input-bg);
|
||||||
|
border-bottom: 1px solid var(--comfy-input-bg);
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-name {
|
||||||
|
flex: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-io {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-column > div {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
height: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-input {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-output {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 0 10px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-switch {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
padding: 2px 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-switch::before,
|
||||||
|
.cn-preview-switch::after {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
color: var(--fg-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-switch::before {
|
||||||
|
content: "◀";
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-switch::after {
|
||||||
|
content: "▶";
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-value {
|
||||||
|
color: var(--descrip-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-string {
|
||||||
|
min-height: 30px;
|
||||||
|
max-height: 300px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--descrip-text);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 3px 5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-preview-description {
|
||||||
|
margin: 0px 10px 10px 10px;
|
||||||
|
padding: 6px;
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--descrip-text);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-tag-list > div {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-install-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 3px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-selected-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-enable {
|
||||||
|
background-color: #333399;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-disable {
|
||||||
|
background-color: #442277;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-update {
|
||||||
|
background-color: #1155AA;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-try-update {
|
||||||
|
background-color: Gray;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-try-fix {
|
||||||
|
background-color: #6495ED;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-import-failed {
|
||||||
|
background-color: #AA1111;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-install {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-try-install {
|
||||||
|
background-color: Gray;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-uninstall {
|
||||||
|
background-color: #993333;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-reinstall {
|
||||||
|
background-color: #993333;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager .cn-btn-switch {
|
||||||
|
background-color: #448833;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cn-btn-loading-bg {
|
||||||
|
0% {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: -105px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager button.cn-btn-loading {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-color: rgb(0 119 207 / 80%);
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager button.cn-btn-loading::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
content: "";
|
||||||
|
width: 500px;
|
||||||
|
height: 100%;
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
rgb(0 119 207 / 30%),
|
||||||
|
rgb(0 119 207 / 30%) 10px,
|
||||||
|
transparent 10px,
|
||||||
|
transparent 15px
|
||||||
|
);
|
||||||
|
animation: cn-btn-loading-bg 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-light .cn-pack-name a {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-light .cm-warn-note {
|
||||||
|
background-color: #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cn-manager-light .cn-btn-install {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
213
js/model-manager.css
Normal file
213
js/model-manager.css
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
.cmm-manager {
|
||||||
|
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||||
|
z-index: 1099;
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--fg-color);
|
||||||
|
font-family: arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager .cmm-flex-auto {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager button {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--input-text);
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-color: var(--border-color);
|
||||||
|
border-style: solid;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager button:disabled,
|
||||||
|
.cmm-manager input:disabled,
|
||||||
|
.cmm-manager select:disabled {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager button:disabled {
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager .cmm-manager-refresh {
|
||||||
|
display: none;
|
||||||
|
background-color: #000080;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager .cmm-manager-stop {
|
||||||
|
display: none;
|
||||||
|
background-color: #500000;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-header label {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-type,
|
||||||
|
.cmm-manager-base,
|
||||||
|
.cmm-manager-filter {
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-keywords {
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
padding: 0 5px 0 26px;
|
||||||
|
background-size: 16px;
|
||||||
|
background-position: 5px center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20pointer-events%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23888%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m21%2021-4.486-4.494M19%2010.5a8.5%208.5%200%201%201-17%200%208.5%208.5%200%200%201%2017%200%22%2F%3E%3C%2Fsvg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-status {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-grid {
|
||||||
|
flex: auto;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-selection {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-grid .tg-turbogrid {
|
||||||
|
font-family: var(--grid-font);
|
||||||
|
font-size: 15px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-grid .cmm-node-name a {
|
||||||
|
color: skyblue;
|
||||||
|
text-decoration: none;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-grid .cmm-node-desc a {
|
||||||
|
color: #5555FF;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-grid .tg-cell a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-icon-passed {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 10px);
|
||||||
|
top: calc(50% - 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager .cmm-btn-enable {
|
||||||
|
background-color: blue;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager .cmm-btn-disable {
|
||||||
|
background-color: MediumSlateBlue;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager .cmm-btn-install {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-btn-download {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 10px);
|
||||||
|
top: calc(50% - 10px);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.8;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-btn-download:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-light .cmm-btn-download {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cmm-btn-loading-bg {
|
||||||
|
0% {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: -105px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager button.cmm-btn-loading {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-color: rgb(0 119 207 / 80%);
|
||||||
|
background-color: var(--comfy-input-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager button.cmm-btn-loading::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
content: "";
|
||||||
|
width: 500px;
|
||||||
|
height: 100%;
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
rgb(0 119 207 / 30%),
|
||||||
|
rgb(0 119 207 / 30%) 10px,
|
||||||
|
transparent 10px,
|
||||||
|
transparent 15px
|
||||||
|
);
|
||||||
|
animation: cmm-btn-loading-bg 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-light .cmm-node-name a {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-light .cm-warn-note {
|
||||||
|
background-color: #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmm-manager-light .cmm-btn-install {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
@@ -1,220 +1,18 @@
|
|||||||
|
import { app } from "../../scripts/app.js";
|
||||||
import { $el } from "../../scripts/ui.js";
|
import { $el } from "../../scripts/ui.js";
|
||||||
import {
|
import {
|
||||||
manager_instance, rebootAPI,
|
manager_instance, rebootAPI,
|
||||||
fetchData, md5, icons
|
fetchData, md5, icons, show_message, customAlert, infoToast, showTerminal,
|
||||||
|
storeColumnWidth, restoreColumnWidth, loadCss
|
||||||
} from "./common.js";
|
} from "./common.js";
|
||||||
|
import { api } from "../../scripts/api.js";
|
||||||
|
|
||||||
// https://cenfun.github.io/turbogrid/api.html
|
// https://cenfun.github.io/turbogrid/api.html
|
||||||
import TG from "./turbogrid.esm.js";
|
import TG from "./turbogrid.esm.js";
|
||||||
|
|
||||||
const pageCss = `
|
loadCss("./model-manager.css");
|
||||||
.cmm-manager {
|
|
||||||
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
||||||
z-index: 1099;
|
|
||||||
width: 80%;
|
|
||||||
height: 80%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
color: var(--fg-color);
|
|
||||||
font-family: arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager .cmm-flex-auto {
|
const gridId = "model";
|
||||||
flex: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager button {
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--input-text);
|
|
||||||
background-color: var(--comfy-input-bg);
|
|
||||||
border-radius: 8px;
|
|
||||||
border-color: var(--border-color);
|
|
||||||
border-style: solid;
|
|
||||||
margin: 0;
|
|
||||||
padding: 4px 8px;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager button:disabled,
|
|
||||||
.cmm-manager input:disabled,
|
|
||||||
.cmm-manager select:disabled {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager button:disabled {
|
|
||||||
background-color: var(--comfy-input-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-header {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 5px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-header label {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-type,
|
|
||||||
.cmm-manager-base,
|
|
||||||
.cmm-manager-filter {
|
|
||||||
height: 28px;
|
|
||||||
line-height: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-keywords {
|
|
||||||
height: 28px;
|
|
||||||
line-height: 28px;
|
|
||||||
padding: 0 5px 0 26px;
|
|
||||||
background-size: 16px;
|
|
||||||
background-position: 5px center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-image: url("data:image/svg+xml;charset=utf8,${encodeURIComponent(icons.search.replace("currentColor", "#888"))}");
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-status {
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-grid {
|
|
||||||
flex: auto;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-selection {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-message {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-footer {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-grid .tg-turbogrid {
|
|
||||||
font-family: var(--grid-font);
|
|
||||||
font-size: 15px;
|
|
||||||
background: var(--bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-grid .cmm-node-name a {
|
|
||||||
color: skyblue;
|
|
||||||
text-decoration: none;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-grid .cmm-node-desc a {
|
|
||||||
color: #5555FF;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-grid .tg-cell a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-icon-passed {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
position: absolute;
|
|
||||||
left: calc(50% - 10px);
|
|
||||||
top: calc(50% - 10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager .cmm-btn-enable {
|
|
||||||
background-color: blue;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager .cmm-btn-disable {
|
|
||||||
background-color: MediumSlateBlue;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager .cmm-btn-install {
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-btn-download {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
position: absolute;
|
|
||||||
left: calc(50% - 10px);
|
|
||||||
top: calc(50% - 10px);
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.8;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-btn-download:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-light .cmm-btn-download {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes cmm-btn-loading-bg {
|
|
||||||
0% {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: -105px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager button.cmm-btn-loading {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border-color: rgb(0 119 207 / 80%);
|
|
||||||
background-color: var(--comfy-input-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager button.cmm-btn-loading::after {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
content: "";
|
|
||||||
width: 500px;
|
|
||||||
height: 100%;
|
|
||||||
background-image: repeating-linear-gradient(
|
|
||||||
-45deg,
|
|
||||||
rgb(0 119 207 / 30%),
|
|
||||||
rgb(0 119 207 / 30%) 10px,
|
|
||||||
transparent 10px,
|
|
||||||
transparent 15px
|
|
||||||
);
|
|
||||||
animation: cmm-btn-loading-bg 2s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-light .cmm-node-name a {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-light .cm-warn-note {
|
|
||||||
background-color: #ccc !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmm-manager-light .cmm-btn-install {
|
|
||||||
background-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const pageHtml = `
|
const pageHtml = `
|
||||||
<div class="cmm-manager-header">
|
<div class="cmm-manager-header">
|
||||||
@@ -235,7 +33,14 @@ const pageHtml = `
|
|||||||
<div class="cmm-manager-selection"></div>
|
<div class="cmm-manager-selection"></div>
|
||||||
<div class="cmm-manager-message"></div>
|
<div class="cmm-manager-message"></div>
|
||||||
<div class="cmm-manager-footer">
|
<div class="cmm-manager-footer">
|
||||||
<button class="cmm-manager-back">Back</button>
|
<button class="cmm-manager-back">
|
||||||
|
<svg class="arrow-icon" width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 8H18M2 8L8 2M2 8L8 14" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button class="cmm-manager-refresh">Refresh</button>
|
||||||
|
<button class="cmm-manager-stop">Stop</button>
|
||||||
<div class="cmm-flex-auto"></div>
|
<div class="cmm-flex-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -254,17 +59,11 @@ export class ModelManager {
|
|||||||
this.keywords = '';
|
this.keywords = '';
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
|
|
||||||
|
api.addEventListener("cm-queue-status", this.onQueueStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
||||||
if (!document.querySelector(`style[context="${this.id}"]`)) {
|
|
||||||
const $style = document.createElement("style");
|
|
||||||
$style.setAttribute("context", this.id);
|
|
||||||
$style.innerHTML = pageCss;
|
|
||||||
document.head.appendChild($style);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.element = $el("div", {
|
this.element = $el("div", {
|
||||||
parent: document.body,
|
parent: document.body,
|
||||||
className: "comfy-modal cmm-manager"
|
className: "comfy-modal cmm-manager"
|
||||||
@@ -282,10 +81,13 @@ export class ModelManager {
|
|||||||
value: ""
|
value: ""
|
||||||
}, {
|
}, {
|
||||||
label: "Installed",
|
label: "Installed",
|
||||||
value: "True"
|
value: "installed"
|
||||||
}, {
|
}, {
|
||||||
label: "Not Installed",
|
label: "Not Installed",
|
||||||
value: "False"
|
value: "not_installed"
|
||||||
|
}, {
|
||||||
|
label: "In Workflow",
|
||||||
|
value: "in_workflow"
|
||||||
}];
|
}];
|
||||||
|
|
||||||
this.typeList = [{
|
this.typeList = [{
|
||||||
@@ -365,12 +167,25 @@ export class ModelManager {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
".cmm-manager-refresh": {
|
||||||
|
click: () => {
|
||||||
|
app.refreshComboInNodes();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
".cmm-manager-stop": {
|
||||||
|
click: () => {
|
||||||
|
api.fetchApi('/manager/queue/reset');
|
||||||
|
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
".cmm-manager-back": {
|
".cmm-manager-back": {
|
||||||
click: (e) => {
|
click: (e) => {
|
||||||
this.close()
|
this.close()
|
||||||
manager_instance.show();
|
manager_instance.show();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
Object.keys(eventsMap).forEach(selector => {
|
Object.keys(eventsMap).forEach(selector => {
|
||||||
const target = this.element.querySelector(selector);
|
const target = this.element.querySelector(selector);
|
||||||
@@ -402,6 +217,10 @@ export class ModelManager {
|
|||||||
this.renderSelected();
|
this.renderSelected();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
grid.bind("onColumnWidthChanged", (e, columnItem) => {
|
||||||
|
storeColumnWidth(gridId, columnItem)
|
||||||
|
});
|
||||||
|
|
||||||
grid.bind('onClick', (e, d) => {
|
grid.bind('onClick', (e, d) => {
|
||||||
const { rowItem } = d;
|
const { rowItem } = d;
|
||||||
const target = d.e.target;
|
const target = d.e.target;
|
||||||
@@ -438,12 +257,31 @@ export class ModelManager {
|
|||||||
rowFilter: (rowItem) => {
|
rowFilter: (rowItem) => {
|
||||||
|
|
||||||
const searchableColumns = ["name", "type", "base", "description", "filename", "save_path"];
|
const searchableColumns = ["name", "type", "base", "description", "filename", "save_path"];
|
||||||
|
const models_extensions = ['.ckpt', '.pt', '.pt2', '.bin', '.pth', '.safetensors', '.pkl', '.sft'];
|
||||||
|
|
||||||
let shouldShown = grid.highlightKeywordsFilter(rowItem, searchableColumns, this.keywords);
|
let shouldShown = grid.highlightKeywordsFilter(rowItem, searchableColumns, this.keywords);
|
||||||
|
|
||||||
if (shouldShown) {
|
if (shouldShown) {
|
||||||
if(this.filter && rowItem.installed !== this.filter) {
|
if(this.filter) {
|
||||||
return false;
|
if (this.filter == "in_workflow") {
|
||||||
|
rowItem.in_workflow = null;
|
||||||
|
if (Array.isArray(app.graph._nodes)) {
|
||||||
|
app.graph._nodes.forEach((item, i) => {
|
||||||
|
if (Array.isArray(item.widgets_values)) {
|
||||||
|
item.widgets_values.forEach((_item, i) => {
|
||||||
|
if (rowItem.in_workflow === null && _item !== null && models_extensions.includes("." + _item.toString().split('.').pop())) {
|
||||||
|
let filename = _item.match(/([^\/]+)(?=\.\w+$)/)[0];
|
||||||
|
if (grid.highlightKeywordsFilter(rowItem, searchableColumns, filename)) {
|
||||||
|
rowItem.in_workflow = "True";
|
||||||
|
grid.highlightKeywordsFilter(rowItem, searchableColumns, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ((this.filter == "installed" && rowItem.installed == "True") || (this.filter == "not_installed" && rowItem.installed == "False") || (this.filter == "in_workflow" && rowItem.in_workflow == "True"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.type && rowItem.type !== this.type) {
|
if(this.type && rowItem.type !== this.type) {
|
||||||
@@ -518,7 +356,7 @@ export class ModelManager {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
formatter: (url, rowItem, columnItem) => {
|
formatter: (url, rowItem, columnItem) => {
|
||||||
return `<a class="cmm-btn-download" title="Download file" href="${url}" target="_blank">${icons.download}</a>`;
|
return `<a class="cmm-btn-download" tooltip="Download file" href="${url}" target="_blank">${icons.download}</a>`;
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
id: 'size',
|
id: 'size',
|
||||||
@@ -553,6 +391,8 @@ export class ModelManager {
|
|||||||
width: 200
|
width: 200
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
restoreColumnWidth(gridId, columns);
|
||||||
|
|
||||||
this.grid.setData({
|
this.grid.setData({
|
||||||
options,
|
options,
|
||||||
rows,
|
rows,
|
||||||
@@ -595,17 +435,27 @@ export class ModelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async installModels(list, btn) {
|
async installModels(list, btn) {
|
||||||
|
let stats = await api.fetchApi('/manager/queue/status');
|
||||||
|
|
||||||
|
stats = await stats.json();
|
||||||
|
if(stats.is_processing) {
|
||||||
|
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
btn.classList.add("cmm-btn-loading");
|
btn.classList.add("cmm-btn-loading");
|
||||||
this.showLoading();
|
|
||||||
this.showError("");
|
this.showError("");
|
||||||
|
|
||||||
let needRestart = false;
|
let needRefresh = false;
|
||||||
let errorMsg = "";
|
let errorMsg = "";
|
||||||
|
|
||||||
|
await api.fetchApi('/manager/queue/reset');
|
||||||
|
|
||||||
|
let target_items = [];
|
||||||
|
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
|
|
||||||
this.grid.scrollRowIntoView(item);
|
this.grid.scrollRowIntoView(item);
|
||||||
|
target_items.push(item);
|
||||||
|
|
||||||
if (!this.focusInstall(item)) {
|
if (!this.focusInstall(item)) {
|
||||||
this.grid.onNextUpdated(() => {
|
this.grid.onNextUpdated(() => {
|
||||||
@@ -616,48 +466,112 @@ export class ModelManager {
|
|||||||
this.showStatus(`Install ${item.name} ...`);
|
this.showStatus(`Install ${item.name} ...`);
|
||||||
|
|
||||||
const data = item.originalData;
|
const data = item.originalData;
|
||||||
const res = await fetchData('/model/install', {
|
data.ui_id = item.hash;
|
||||||
|
|
||||||
|
const res = await api.fetchApi(`/manager/queue/install_model`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.status != 200) {
|
||||||
|
errorMsg = `'${item.name}': `;
|
||||||
|
|
||||||
if (res.error) {
|
if(res.status == 403) {
|
||||||
errorMsg = `Install failed: ${item.name} ${res.error.message}`;
|
errorMsg += `This action is not allowed with this security level configuration.\n`;
|
||||||
break;;
|
} else {
|
||||||
|
errorMsg += await res.text() + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
needRestart = true;
|
this.install_context = {btn: btn, targets: target_items};
|
||||||
|
|
||||||
this.grid.setRowSelected(item, false);
|
if(errorMsg) {
|
||||||
|
this.showError(errorMsg);
|
||||||
|
show_message("[Installation Errors]\n"+errorMsg);
|
||||||
|
|
||||||
|
// reset
|
||||||
|
for(let k in target_items) {
|
||||||
|
const item = target_items[k];
|
||||||
|
this.grid.updateCell(item, "installed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await api.fetchApi('/manager/queue/start');
|
||||||
|
this.showStop();
|
||||||
|
showTerminal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onQueueStatus(event) {
|
||||||
|
let self = ModelManager.instance;
|
||||||
|
|
||||||
|
if(event.detail.status == 'in_progress' && event.detail.ui_target == 'model_manager') {
|
||||||
|
const hash = event.detail.target;
|
||||||
|
|
||||||
|
const item = self.grid.getRowItemBy("hash", hash);
|
||||||
|
|
||||||
item.refresh = true;
|
item.refresh = true;
|
||||||
|
self.grid.setRowSelected(item, false);
|
||||||
item.selectable = false;
|
item.selectable = false;
|
||||||
this.grid.updateCell(item, "installed");
|
// self.grid.updateCell(item, "tg-column-select");
|
||||||
this.grid.updateCell(item, "tg-column-select");
|
self.grid.updateRow(item);
|
||||||
|
}
|
||||||
|
else if(event.detail.status == 'done') {
|
||||||
|
self.hideStop();
|
||||||
|
self.onQueueCompleted(event.detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.showStatus(`Install ${item.name} successfully`);
|
async onQueueCompleted(info) {
|
||||||
|
let result = info.model_result;
|
||||||
|
|
||||||
|
if(result.length == 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hideLoading();
|
let self = ModelManager.instance;
|
||||||
|
|
||||||
|
if(!self.install_context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let btn = self.install_context.btn;
|
||||||
|
|
||||||
|
self.hideLoading();
|
||||||
btn.classList.remove("cmm-btn-loading");
|
btn.classList.remove("cmm-btn-loading");
|
||||||
|
|
||||||
|
let errorMsg = "";
|
||||||
|
|
||||||
|
for(let hash in result){
|
||||||
|
let v = result[hash];
|
||||||
|
|
||||||
|
if(v != 'success')
|
||||||
|
errorMsg += v + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let k in self.install_context.targets) {
|
||||||
|
let item = self.install_context.targets[k];
|
||||||
|
self.grid.updateCell(item, "installed");
|
||||||
|
}
|
||||||
|
|
||||||
if (errorMsg) {
|
if (errorMsg) {
|
||||||
this.showError(errorMsg);
|
self.showError(errorMsg);
|
||||||
|
show_message("Installation Error:\n"+errorMsg);
|
||||||
} else {
|
} else {
|
||||||
this.showStatus(`Install ${list.length} models successfully`);
|
self.showStatus(`Install ${result.length} models successfully`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needRestart) {
|
self.showRefresh();
|
||||||
this.showMessage(`To apply the installed model, please click the 'Refresh' button on the main menu.`, "red")
|
self.showMessage(`To apply the installed model, please click the 'Refresh' button.`, "red")
|
||||||
}
|
|
||||||
|
|
||||||
|
infoToast('Tasks done', `[ComfyUI-Manager] All model downloading tasks in the queue have been completed.\n${info.done_count}/${info.total_count}`);
|
||||||
|
self.install_context = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
getModelList(models) {
|
getModelList(models) {
|
||||||
|
|
||||||
const typeMap = new Map();
|
const typeMap = new Map();
|
||||||
const baseMap = new Map();
|
const baseMap = new Map();
|
||||||
|
|
||||||
@@ -826,7 +740,7 @@ export class ModelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoading() {
|
showLoading() {
|
||||||
this.setDisabled(true);
|
// this.setDisabled(true);
|
||||||
if (this.grid) {
|
if (this.grid) {
|
||||||
this.grid.showLoading();
|
this.grid.showLoading();
|
||||||
this.grid.showMask({
|
this.grid.showMask({
|
||||||
@@ -836,7 +750,7 @@ export class ModelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hideLoading() {
|
hideLoading() {
|
||||||
this.setDisabled(false);
|
// this.setDisabled(false);
|
||||||
if (this.grid) {
|
if (this.grid) {
|
||||||
this.grid.hideLoading();
|
this.grid.hideLoading();
|
||||||
this.grid.hideMask();
|
this.grid.hideMask();
|
||||||
@@ -844,8 +758,9 @@ export class ModelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDisabled(disabled) {
|
setDisabled(disabled) {
|
||||||
|
|
||||||
const $close = this.element.querySelector(".cmm-manager-close");
|
const $close = this.element.querySelector(".cmm-manager-close");
|
||||||
|
const $refresh = this.element.querySelector(".cmm-manager-refresh");
|
||||||
|
const $stop = this.element.querySelector(".cmm-manager-stop");
|
||||||
|
|
||||||
const list = [
|
const list = [
|
||||||
".cmm-manager-header input",
|
".cmm-manager-header input",
|
||||||
@@ -857,7 +772,7 @@ export class ModelManager {
|
|||||||
})
|
})
|
||||||
.flat()
|
.flat()
|
||||||
.filter(it => {
|
.filter(it => {
|
||||||
return it !== $close;
|
return it !== $close && it !== $refresh && it !== $stop;
|
||||||
});
|
});
|
||||||
|
|
||||||
list.forEach($elem => {
|
list.forEach($elem => {
|
||||||
@@ -874,6 +789,18 @@ export class ModelManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showRefresh() {
|
||||||
|
this.element.querySelector(".cmm-manager-refresh").style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
showStop() {
|
||||||
|
this.element.querySelector(".cmm-manager-stop").style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
hideStop() {
|
||||||
|
this.element.querySelector(".cmm-manager-stop").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
setKeywords(keywords = "") {
|
setKeywords(keywords = "") {
|
||||||
this.keywords = keywords;
|
this.keywords = keywords;
|
||||||
this.element.querySelector(".cmm-manager-keywords").value = keywords;
|
this.element.querySelector(".cmm-manager-keywords").value = keywords;
|
||||||
@@ -890,4 +817,4 @@ export class ModelManager {
|
|||||||
close() {
|
close() {
|
||||||
this.element.style.display = "none";
|
this.element.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
|
|
||||||
let double_click_policy = "copy-all";
|
|
||||||
|
|
||||||
api.fetchApi('/manager/dbl_click/policy')
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(data => set_double_click_policy(data));
|
|
||||||
|
|
||||||
export function set_double_click_policy(mode) {
|
|
||||||
double_click_policy = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMenuHandler(nodeType, cb) {
|
function addMenuHandler(nodeType, cb) {
|
||||||
const getOpts = nodeType.prototype.getExtraMenuOptions;
|
const getOpts = nodeType.prototype.getExtraMenuOptions;
|
||||||
nodeType.prototype.getExtraMenuOptions = function () {
|
nodeType.prototype.getExtraMenuOptions = function () {
|
||||||
@@ -153,62 +143,6 @@ function node_info_copy(src, dest, connect_both, copy_shape) {
|
|||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.Manager.NodeFixer",
|
name: "Comfy.Manager.NodeFixer",
|
||||||
|
|
||||||
async nodeCreated(node, app) {
|
|
||||||
let orig_dblClick = node.onDblClick;
|
|
||||||
node.onDblClick = function (e, pos, self) {
|
|
||||||
orig_dblClick?.apply?.(this, arguments);
|
|
||||||
|
|
||||||
if((!node.inputs && !node.outputs) || pos[1] > 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch(double_click_policy) {
|
|
||||||
case "copy-all":
|
|
||||||
case "copy-full":
|
|
||||||
case "copy-input":
|
|
||||||
{
|
|
||||||
if(node.inputs?.some(x => x.link != null) || node.outputs?.some(x => x.links != null && x.links.length > 0) )
|
|
||||||
return;
|
|
||||||
|
|
||||||
let src_node = lookup_nearest_nodes(node);
|
|
||||||
if(src_node)
|
|
||||||
{
|
|
||||||
let both_connection = double_click_policy != "copy-input";
|
|
||||||
let copy_shape = double_click_policy == "copy-full";
|
|
||||||
node_info_copy(src_node, node, both_connection, copy_shape);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "possible-input":
|
|
||||||
{
|
|
||||||
let nearest_inputs = lookup_nearest_inputs(node);
|
|
||||||
if(nearest_inputs)
|
|
||||||
connect_inputs(nearest_inputs, node);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "dual":
|
|
||||||
{
|
|
||||||
if(pos[0] < node.size[0]/2) {
|
|
||||||
// left: possible-input
|
|
||||||
let nearest_inputs = lookup_nearest_inputs(node);
|
|
||||||
if(nearest_inputs)
|
|
||||||
connect_inputs(nearest_inputs, node);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// right: copy-all
|
|
||||||
if(node.inputs?.some(x => x.link != null) || node.outputs?.some(x => x.links != null && x.links.length > 0) )
|
|
||||||
return;
|
|
||||||
|
|
||||||
let src_node = lookup_nearest_nodes(node);
|
|
||||||
if(src_node)
|
|
||||||
node_info_copy(src_node, node, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeRegisterNodeDef(nodeType, nodeData, app) {
|
beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
addMenuHandler(nodeType, function (_, options) {
|
addMenuHandler(nodeType, function (_, options) {
|
||||||
options.push({
|
options.push({
|
||||||
|
|||||||
619
js/popover-helper.js
Normal file
619
js/popover-helper.js
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
const hasOwn = function(obj, key) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNum = function(num) {
|
||||||
|
if (typeof num !== 'number' || isNaN(num)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isInvalid = function(n) {
|
||||||
|
if (n === Number.MAX_VALUE || n === Number.MIN_VALUE || n === Number.NEGATIVE_INFINITY || n === Number.POSITIVE_INFINITY) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (isInvalid(num)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toNum = (num) => {
|
||||||
|
if (typeof (num) !== 'number') {
|
||||||
|
num = parseFloat(num);
|
||||||
|
}
|
||||||
|
if (isNaN(num)) {
|
||||||
|
num = 0;
|
||||||
|
}
|
||||||
|
num = Math.round(num);
|
||||||
|
return num;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clamp = function(value, min, max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWindow = (obj) => {
|
||||||
|
return Boolean(obj && obj === obj.window);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDocument = (obj) => {
|
||||||
|
return Boolean(obj && obj.nodeType === 9);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isElement = (obj) => {
|
||||||
|
return Boolean(obj && obj.nodeType === 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
export const toRect = (obj) => {
|
||||||
|
if (obj) {
|
||||||
|
return {
|
||||||
|
left: toNum(obj.left || obj.x),
|
||||||
|
top: toNum(obj.top || obj.y),
|
||||||
|
width: toNum(obj.width),
|
||||||
|
height: toNum(obj.height)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getElement = (selector) => {
|
||||||
|
if (typeof selector === 'string' && selector) {
|
||||||
|
if (selector.startsWith('#')) {
|
||||||
|
return document.getElementById(selector.slice(1));
|
||||||
|
}
|
||||||
|
return document.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDocument(selector)) {
|
||||||
|
return selector.body;
|
||||||
|
}
|
||||||
|
if (isElement(selector)) {
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRect = (target, fixed) => {
|
||||||
|
if (!target) {
|
||||||
|
return toRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWindow(target)) {
|
||||||
|
return {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const elem = getElement(target);
|
||||||
|
if (!elem) {
|
||||||
|
return toRect(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
const br = elem.getBoundingClientRect();
|
||||||
|
const rect = toRect(br);
|
||||||
|
|
||||||
|
// fix offset
|
||||||
|
if (!fixed) {
|
||||||
|
rect.left += window.scrollX;
|
||||||
|
rect.top += window.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect.width = elem.offsetWidth;
|
||||||
|
rect.height = elem.offsetHeight;
|
||||||
|
|
||||||
|
return rect;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
const calculators = {
|
||||||
|
|
||||||
|
bottom: (info, containerRect, targetRect) => {
|
||||||
|
info.space = containerRect.top + containerRect.height - targetRect.top - targetRect.height - info.height;
|
||||||
|
info.top = targetRect.top + targetRect.height;
|
||||||
|
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5);
|
||||||
|
},
|
||||||
|
|
||||||
|
top: (info, containerRect, targetRect) => {
|
||||||
|
info.space = targetRect.top - info.height - containerRect.top;
|
||||||
|
info.top = targetRect.top - info.height;
|
||||||
|
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5);
|
||||||
|
},
|
||||||
|
|
||||||
|
right: (info, containerRect, targetRect) => {
|
||||||
|
info.space = containerRect.left + containerRect.width - targetRect.left - targetRect.width - info.width;
|
||||||
|
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5);
|
||||||
|
info.left = targetRect.left + targetRect.width;
|
||||||
|
},
|
||||||
|
|
||||||
|
left: (info, containerRect, targetRect) => {
|
||||||
|
info.space = targetRect.left - info.width - containerRect.left;
|
||||||
|
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5);
|
||||||
|
info.left = targetRect.left - info.width;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// with order
|
||||||
|
export const getDefaultPositions = () => {
|
||||||
|
return Object.keys(calculators);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSpace = (info, containerRect, targetRect) => {
|
||||||
|
const calculator = calculators[info.position];
|
||||||
|
calculator(info, containerRect, targetRect);
|
||||||
|
if (info.space >= 0) {
|
||||||
|
info.passed += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
const calculateAlignOffset = (info, containerRect, targetRect, alignType, sizeType) => {
|
||||||
|
|
||||||
|
const popoverStart = info[alignType];
|
||||||
|
const popoverSize = info[sizeType];
|
||||||
|
|
||||||
|
const containerStart = containerRect[alignType];
|
||||||
|
const containerSize = containerRect[sizeType];
|
||||||
|
|
||||||
|
const targetStart = targetRect[alignType];
|
||||||
|
const targetSize = targetRect[sizeType];
|
||||||
|
|
||||||
|
const targetCenter = targetStart + targetSize * 0.5;
|
||||||
|
|
||||||
|
// size overflow
|
||||||
|
if (popoverSize > containerSize) {
|
||||||
|
const overflow = (popoverSize - containerSize) * 0.5;
|
||||||
|
info[alignType] = containerStart - overflow;
|
||||||
|
info.offset = targetCenter - containerStart + overflow;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const space1 = popoverStart - containerStart;
|
||||||
|
const space2 = (containerStart + containerSize) - (popoverStart + popoverSize);
|
||||||
|
|
||||||
|
// both side passed, default to center
|
||||||
|
if (space1 >= 0 && space2 >= 0) {
|
||||||
|
if (info.passed) {
|
||||||
|
info.passed += 2;
|
||||||
|
}
|
||||||
|
info.offset = popoverSize * 0.5;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// one side passed
|
||||||
|
if (info.passed) {
|
||||||
|
info.passed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (space1 < 0) {
|
||||||
|
const min = containerStart;
|
||||||
|
info[alignType] = min;
|
||||||
|
info.offset = targetCenter - min;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// space2 < 0
|
||||||
|
const max = containerStart + containerSize - popoverSize;
|
||||||
|
info[alignType] = max;
|
||||||
|
info.offset = targetCenter - max;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateHV = (info, containerRect) => {
|
||||||
|
if (['top', 'bottom'].includes(info.position)) {
|
||||||
|
info.top = clamp(info.top, containerRect.top, containerRect.top + containerRect.height - info.height);
|
||||||
|
return ['left', 'width'];
|
||||||
|
}
|
||||||
|
info.left = clamp(info.left, containerRect.left, containerRect.left + containerRect.width - info.width);
|
||||||
|
return ['top', 'height'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateOffset = (info, containerRect, targetRect) => {
|
||||||
|
|
||||||
|
const [alignType, sizeType] = calculateHV(info, containerRect);
|
||||||
|
|
||||||
|
calculateAlignOffset(info, containerRect, targetRect, alignType, sizeType);
|
||||||
|
|
||||||
|
info.offset = clamp(info.offset, 0, info[sizeType]);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
const calculateDistance = (info, previousPositionInfo) => {
|
||||||
|
if (!previousPositionInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// no change if position no change with previous
|
||||||
|
if (info.position === previousPositionInfo.position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ax = info.left + info.width * 0.5;
|
||||||
|
const ay = info.top + info.height * 0.5;
|
||||||
|
const bx = previousPositionInfo.left + previousPositionInfo.width * 0.5;
|
||||||
|
const by = previousPositionInfo.top + previousPositionInfo.height * 0.5;
|
||||||
|
const dx = Math.abs(ax - bx);
|
||||||
|
const dy = Math.abs(ay - by);
|
||||||
|
info.distance = Math.round(Math.sqrt(dx * dx + dy * dy));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
const calculatePositionInfo = (info, containerRect, targetRect, previousPositionInfo) => {
|
||||||
|
calculateSpace(info, containerRect, targetRect);
|
||||||
|
calculateOffset(info, containerRect, targetRect);
|
||||||
|
calculateDistance(info, previousPositionInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
const calculateBestPosition = (containerRect, targetRect, infoMap, withOrder, previousPositionInfo) => {
|
||||||
|
|
||||||
|
// position space: +1
|
||||||
|
// align space:
|
||||||
|
// two side passed: +2
|
||||||
|
// one side passed: +1
|
||||||
|
|
||||||
|
const safePassed = 3;
|
||||||
|
|
||||||
|
if (previousPositionInfo) {
|
||||||
|
const prevInfo = infoMap[previousPositionInfo.position];
|
||||||
|
if (prevInfo) {
|
||||||
|
calculatePositionInfo(prevInfo, containerRect, targetRect);
|
||||||
|
if (prevInfo.passed >= safePassed) {
|
||||||
|
return prevInfo;
|
||||||
|
}
|
||||||
|
prevInfo.calculated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionList = [];
|
||||||
|
Object.values(infoMap).forEach((info) => {
|
||||||
|
if (!info.calculated) {
|
||||||
|
calculatePositionInfo(info, containerRect, targetRect, previousPositionInfo);
|
||||||
|
}
|
||||||
|
positionList.push(info);
|
||||||
|
});
|
||||||
|
|
||||||
|
positionList.sort((a, b) => {
|
||||||
|
if (a.passed !== b.passed) {
|
||||||
|
return b.passed - a.passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withOrder && a.passed >= safePassed && b.passed >= safePassed) {
|
||||||
|
return a.index - b.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.space !== b.space) {
|
||||||
|
return b.space - a.space;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.index - b.index;
|
||||||
|
});
|
||||||
|
|
||||||
|
// logTable(positionList);
|
||||||
|
|
||||||
|
return positionList[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
// const logTable = (() => {
|
||||||
|
// let time_id;
|
||||||
|
// return (info) => {
|
||||||
|
// clearTimeout(time_id);
|
||||||
|
// time_id = setTimeout(() => {
|
||||||
|
// console.table(info);
|
||||||
|
// }, 10);
|
||||||
|
// };
|
||||||
|
// })();
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
const getAllowPositions = (positions, defaultAllowPositions) => {
|
||||||
|
if (!positions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(positions)) {
|
||||||
|
positions = positions.join(',');
|
||||||
|
}
|
||||||
|
positions = String(positions).split(',').map((it) => it.trim().toLowerCase()).filter((it) => it);
|
||||||
|
positions = positions.filter((it) => defaultAllowPositions.includes(it));
|
||||||
|
if (!positions.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPositionChanged = (info, previousPositionInfo) => {
|
||||||
|
if (!previousPositionInfo) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.left !== previousPositionInfo.left) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.top !== previousPositionInfo.top) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
// const log = (name, time) => {
|
||||||
|
// if (time > 0.1) {
|
||||||
|
// console.log(name, time);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const getBestPosition = (containerRect, targetRect, popoverRect, positions, previousPositionInfo) => {
|
||||||
|
|
||||||
|
const defaultAllowPositions = getDefaultPositions();
|
||||||
|
let withOrder = true;
|
||||||
|
let allowPositions = getAllowPositions(positions, defaultAllowPositions);
|
||||||
|
if (!allowPositions) {
|
||||||
|
allowPositions = defaultAllowPositions;
|
||||||
|
withOrder = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('withOrder', withOrder);
|
||||||
|
|
||||||
|
// const start_time = performance.now();
|
||||||
|
|
||||||
|
const infoMap = {};
|
||||||
|
allowPositions.forEach((k, i) => {
|
||||||
|
infoMap[k] = {
|
||||||
|
position: k,
|
||||||
|
index: i,
|
||||||
|
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: popoverRect.width,
|
||||||
|
height: popoverRect.height,
|
||||||
|
|
||||||
|
space: 0,
|
||||||
|
|
||||||
|
offset: 0,
|
||||||
|
passed: 0,
|
||||||
|
|
||||||
|
distance: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// log('infoMap', performance.now() - start_time);
|
||||||
|
|
||||||
|
|
||||||
|
const bestPosition = calculateBestPosition(containerRect, targetRect, infoMap, withOrder, previousPositionInfo);
|
||||||
|
|
||||||
|
// check left/top
|
||||||
|
bestPosition.changed = isPositionChanged(bestPosition, previousPositionInfo);
|
||||||
|
|
||||||
|
return bestPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
const getTemplatePath = (width, height, arrowOffset, arrowSize, borderRadius) => {
|
||||||
|
const p = (px, py) => {
|
||||||
|
return [px, py].join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
const px = function(num, alignEnd) {
|
||||||
|
const floor = Math.floor(num);
|
||||||
|
let n = num < floor + 0.5 ? floor + 0.5 : floor + 1.5;
|
||||||
|
if (alignEnd) {
|
||||||
|
n -= 1;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pxe = function(num) {
|
||||||
|
return px(num, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ls = [];
|
||||||
|
|
||||||
|
const innerLeft = px(arrowSize);
|
||||||
|
const innerRight = pxe(width - arrowSize);
|
||||||
|
arrowOffset = clamp(arrowOffset, innerLeft, innerRight);
|
||||||
|
|
||||||
|
const innerTop = px(arrowSize);
|
||||||
|
const innerBottom = pxe(height - arrowSize);
|
||||||
|
|
||||||
|
const startPoint = p(innerLeft, innerTop + borderRadius);
|
||||||
|
const arrowPoint = p(arrowOffset, 1);
|
||||||
|
|
||||||
|
const LT = p(innerLeft, innerTop);
|
||||||
|
const RT = p(innerRight, innerTop);
|
||||||
|
|
||||||
|
const AOT = p(arrowOffset - arrowSize, innerTop);
|
||||||
|
const RRT = p(innerRight - borderRadius, innerTop);
|
||||||
|
|
||||||
|
ls.push(`M${startPoint}`);
|
||||||
|
ls.push(`V${innerBottom - borderRadius}`);
|
||||||
|
ls.push(`Q${p(innerLeft, innerBottom)} ${p(innerLeft + borderRadius, innerBottom)}`);
|
||||||
|
ls.push(`H${innerRight - borderRadius}`);
|
||||||
|
ls.push(`Q${p(innerRight, innerBottom)} ${p(innerRight, innerBottom - borderRadius)}`);
|
||||||
|
ls.push(`V${innerTop + borderRadius}`);
|
||||||
|
|
||||||
|
if (arrowOffset < innerLeft + arrowSize + borderRadius) {
|
||||||
|
ls.push(`Q${RT} ${RRT}`);
|
||||||
|
ls.push(`H${arrowOffset + arrowSize}`);
|
||||||
|
ls.push(`L${arrowPoint}`);
|
||||||
|
if (arrowOffset < innerLeft + arrowSize) {
|
||||||
|
ls.push(`L${LT}`);
|
||||||
|
ls.push(`L${startPoint}`);
|
||||||
|
} else {
|
||||||
|
ls.push(`L${AOT}`);
|
||||||
|
ls.push(`Q${LT} ${startPoint}`);
|
||||||
|
}
|
||||||
|
} else if (arrowOffset > innerRight - arrowSize - borderRadius) {
|
||||||
|
if (arrowOffset > innerRight - arrowSize) {
|
||||||
|
ls.push(`L${RT}`);
|
||||||
|
} else {
|
||||||
|
ls.push(`Q${RT} ${p(arrowOffset + arrowSize, innerTop)}`);
|
||||||
|
}
|
||||||
|
ls.push(`L${arrowPoint}`);
|
||||||
|
ls.push(`L${AOT}`);
|
||||||
|
ls.push(`H${innerLeft + borderRadius}`);
|
||||||
|
ls.push(`Q${LT} ${startPoint}`);
|
||||||
|
} else {
|
||||||
|
ls.push(`Q${RT} ${RRT}`);
|
||||||
|
ls.push(`H${arrowOffset + arrowSize}`);
|
||||||
|
ls.push(`L${arrowPoint}`);
|
||||||
|
ls.push(`L${AOT}`);
|
||||||
|
ls.push(`H${innerLeft + borderRadius}`);
|
||||||
|
ls.push(`Q${LT} ${startPoint}`);
|
||||||
|
}
|
||||||
|
return ls.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPathData = function(position, width, height, arrowOffset, arrowSize, borderRadius) {
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
|
||||||
|
bottom: () => {
|
||||||
|
const d = getTemplatePath(width, height, arrowOffset, arrowSize, borderRadius);
|
||||||
|
return {
|
||||||
|
d,
|
||||||
|
transform: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
top: () => {
|
||||||
|
const d = getTemplatePath(width, height, width - arrowOffset, arrowSize, borderRadius);
|
||||||
|
return {
|
||||||
|
d,
|
||||||
|
transform: `rotate(180,${width * 0.5},${height * 0.5})`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
left: () => {
|
||||||
|
const d = getTemplatePath(height, width, arrowOffset, arrowSize, borderRadius);
|
||||||
|
const x = (width - height) * 0.5;
|
||||||
|
const y = (height - width) * 0.5;
|
||||||
|
return {
|
||||||
|
d,
|
||||||
|
transform: `translate(${x} ${y}) rotate(90,${height * 0.5},${width * 0.5})`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
right: () => {
|
||||||
|
const d = getTemplatePath(height, width, height - arrowOffset, arrowSize, borderRadius);
|
||||||
|
const x = (width - height) * 0.5;
|
||||||
|
const y = (height - width) * 0.5;
|
||||||
|
return {
|
||||||
|
d,
|
||||||
|
transform: `translate(${x} ${y}) rotate(-90,${height * 0.5},${width * 0.5})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return handlers[position]();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
// position style cache
|
||||||
|
const styleCache = {
|
||||||
|
// position: '',
|
||||||
|
// top: {},
|
||||||
|
// bottom: {},
|
||||||
|
// left: {},
|
||||||
|
// right: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPositionStyle = (info, options = {}) => {
|
||||||
|
|
||||||
|
const o = {
|
||||||
|
bgColor: '#fff',
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderRadius: 5,
|
||||||
|
arrowSize: 10
|
||||||
|
};
|
||||||
|
Object.keys(o).forEach((k) => {
|
||||||
|
|
||||||
|
if (hasOwn(options, k)) {
|
||||||
|
const d = o[k];
|
||||||
|
const v = options[k];
|
||||||
|
|
||||||
|
if (typeof d === 'string') {
|
||||||
|
// string
|
||||||
|
if (typeof v === 'string' && v) {
|
||||||
|
o[k] = v;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// number
|
||||||
|
if (isNum(v) && v >= 0) {
|
||||||
|
o[k] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const key = [
|
||||||
|
info.width,
|
||||||
|
info.height,
|
||||||
|
info.offset,
|
||||||
|
o.arrowSize,
|
||||||
|
o.borderRadius,
|
||||||
|
o.bgColor,
|
||||||
|
o.borderColor
|
||||||
|
].join('-');
|
||||||
|
|
||||||
|
const positionCache = styleCache[info.position];
|
||||||
|
if (positionCache && key === positionCache.key) {
|
||||||
|
const st = positionCache.style;
|
||||||
|
st.changed = styleCache.position !== info.position;
|
||||||
|
styleCache.position = info.position;
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(options);
|
||||||
|
|
||||||
|
const data = getPathData(info.position, info.width, info.height, info.offset, o.arrowSize, o.borderRadius);
|
||||||
|
// console.log(data);
|
||||||
|
|
||||||
|
const viewBox = [0, 0, info.width, info.height].join(' ');
|
||||||
|
const svg = [
|
||||||
|
`<svg viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">`,
|
||||||
|
`<path d="${data.d}" fill="${o.bgColor}" stroke="${o.borderColor}" transform="${data.transform}" />`,
|
||||||
|
'</svg>'
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
// console.log(svg);
|
||||||
|
const backgroundImage = `url("data:image/svg+xml;charset=utf8,${encodeURIComponent(svg)}")`;
|
||||||
|
|
||||||
|
const background = `${backgroundImage} center no-repeat`;
|
||||||
|
|
||||||
|
const padding = `${o.arrowSize + o.borderRadius}px`;
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
background,
|
||||||
|
backgroundImage,
|
||||||
|
padding,
|
||||||
|
changed: true
|
||||||
|
};
|
||||||
|
|
||||||
|
styleCache.position = info.position;
|
||||||
|
styleCache[info.position] = {
|
||||||
|
key,
|
||||||
|
style
|
||||||
|
};
|
||||||
|
|
||||||
|
return style;
|
||||||
|
};
|
||||||
@@ -3,12 +3,21 @@
|
|||||||
* - custom node pack version to all custom nodes used in the workflow
|
* - custom node pack version to all custom nodes used in the workflow
|
||||||
*
|
*
|
||||||
* Example metadata:
|
* Example metadata:
|
||||||
"extra": {
|
* "nodes": {
|
||||||
"node_versions": {
|
* "1": {
|
||||||
"comfy-core": "v0.3.8-4-g0b2eb7f",
|
* type: "CheckpointLoaderSimple",
|
||||||
"comfyui-easy-use": "1.2.5"
|
* ...
|
||||||
}
|
* properties: {
|
||||||
},
|
* cnr_id: "comfy-core",
|
||||||
|
* version: "0.3.8",
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @typedef {Object} NodeInfo
|
||||||
|
* @property {string} ver - Version (git hash or semantic version)
|
||||||
|
* @property {string} cnr_id - ComfyRegistry node ID
|
||||||
|
* @property {boolean} enabled - Whether the node is enabled
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
@@ -23,7 +32,7 @@ class WorkflowMetadataExtension {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the installed nodes info
|
* Get the installed nodes info
|
||||||
* @returns {Promise<Record<string, {ver: string, cnr_id: string, enabled: boolean}>>} The mapping from node name to its info.
|
* @returns {Promise<Record<string, NodeInfo>>} The mapping from node name to its info.
|
||||||
* ver can either be a git commit hash or a semantic version such as "1.0.0"
|
* ver can either be a git commit hash or a semantic version such as "1.0.0"
|
||||||
* cnr_id is the id of the node in the ComfyRegistry
|
* cnr_id is the id of the node in the ComfyRegistry
|
||||||
* enabled is true if the node is enabled, false if it is disabled
|
* enabled is true if the node is enabled, false if it is disabled
|
||||||
@@ -33,61 +42,42 @@ class WorkflowMetadataExtension {
|
|||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the node versions for the given graph
|
|
||||||
* @param {LGraph} graph The graph to get the node versions for
|
|
||||||
* @returns {Promise<Record<string, string>>} The mapping from node name to version
|
|
||||||
*/
|
|
||||||
getGraphNodeVersions(graph) {
|
|
||||||
const nodeVersions = {};
|
|
||||||
for (const node of graph.nodes) {
|
|
||||||
const nodeData = node.constructor.nodeData;
|
|
||||||
// Frontend only nodes don't have nodeData
|
|
||||||
if (!nodeData) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const modules = nodeData.python_module.split(".");
|
|
||||||
|
|
||||||
if (modules[0] === "custom_nodes") {
|
|
||||||
const nodePackageName = modules[1];
|
|
||||||
const nodeInfo =
|
|
||||||
this.installedNodes[nodePackageName] ??
|
|
||||||
this.installedNodes[nodePackageName.toLowerCase()];
|
|
||||||
if (nodeInfo) {
|
|
||||||
nodeVersions[nodePackageName] = nodeInfo.ver;
|
|
||||||
}
|
|
||||||
} else if (["nodes", "comfy_extras"].includes(modules[0])) {
|
|
||||||
nodeVersions["comfy-core"] = this.comfyCoreVersion;
|
|
||||||
} else {
|
|
||||||
console.warn(`Unknown node source: ${nodeData.python_module}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodeVersions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const extension = this;
|
|
||||||
this.installedNodes = await this.getInstalledNodes();
|
this.installedNodes = await this.getInstalledNodes();
|
||||||
this.comfyCoreVersion = (await api.getSystemStats()).system.comfyui_version;
|
this.comfyCoreVersion = (await api.getSystemStats()).system.comfyui_version;
|
||||||
|
}
|
||||||
|
|
||||||
// Attach metadata when app.graphToPrompt is called.
|
/**
|
||||||
const originalSerialize = LGraph.prototype.serialize;
|
* Called when any node is created
|
||||||
LGraph.prototype.serialize = function () {
|
* @param {LGraphNode} node The newly created node
|
||||||
const workflow = originalSerialize.apply(this, arguments);
|
*/
|
||||||
|
nodeCreated(node) {
|
||||||
|
try {
|
||||||
|
// nodeData doesn't exist if node is missing or node is frontend only node
|
||||||
|
if (!node?.constructor?.nodeData?.python_module) return;
|
||||||
|
|
||||||
// Add metadata to the workflow
|
const nodeProperties = (node.properties ??= {});
|
||||||
if (!workflow.extra) {
|
const modules = node.constructor.nodeData.python_module.split(".");
|
||||||
workflow.extra = {};
|
const moduleType = modules[0];
|
||||||
|
|
||||||
|
if (moduleType === "custom_nodes") {
|
||||||
|
const nodePackageName = modules[1];
|
||||||
|
const { cnr_id, aux_id, ver } =
|
||||||
|
this.installedNodes[nodePackageName] ??
|
||||||
|
this.installedNodes[nodePackageName.toLowerCase()] ??
|
||||||
|
{};
|
||||||
|
|
||||||
|
if (cnr_id === "comfy-core") return; // don't allow hijacking comfy-core name
|
||||||
|
if (cnr_id) nodeProperties.cnr_id = cnr_id;
|
||||||
|
else nodeProperties.aux_id = aux_id;
|
||||||
|
if (ver) nodeProperties.ver = ver.trim();
|
||||||
|
} else if (["nodes", "comfy_extras", "comfy_api_nodes"].includes(moduleType)) {
|
||||||
|
nodeProperties.cnr_id = "comfy-core";
|
||||||
|
nodeProperties.ver = this.comfyCoreVersion;
|
||||||
}
|
}
|
||||||
const graph = this;
|
} catch (e) {
|
||||||
try {
|
console.error(e);
|
||||||
workflow.extra["node_versions"] = extension.getGraphNodeVersions(graph);
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return workflow;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
739
model-list.json
739
model-list.json
@@ -209,28 +209,6 @@
|
|||||||
"url": "https://huggingface.co/stabilityai/stable-diffusion-x4-upscaler/resolve/main/x4-upscaler-ema.safetensors",
|
"url": "https://huggingface.co/stabilityai/stable-diffusion-x4-upscaler/resolve/main/x4-upscaler-ema.safetensors",
|
||||||
"size": "3.53GB"
|
"size": "3.53GB"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Inswapper-fp16 (face swap)",
|
|
||||||
"type": "insightface",
|
|
||||||
"base": "inswapper",
|
|
||||||
"save_path": "insightface",
|
|
||||||
"description": "Checkpoint of the insightface swapper model\n(used by ComfyUI-FaceSwap, comfyui-reactor-node, CharacterFaceSwap,\nComfyUI roop and comfy_mtb)",
|
|
||||||
"reference": "https://github.com/facefusion/facefusion-assets",
|
|
||||||
"filename": "inswapper_128_fp16.onnx",
|
|
||||||
"url": "https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128_fp16.onnx",
|
|
||||||
"size": "277.7MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Inswapper (face swap)",
|
|
||||||
"type": "insightface",
|
|
||||||
"base": "inswapper",
|
|
||||||
"save_path": "insightface",
|
|
||||||
"description": "Checkpoint of the insightface swapper model\n(used by ComfyUI-FaceSwap, comfyui-reactor-node, CharacterFaceSwap,\nComfyUI roop and comfy_mtb)",
|
|
||||||
"reference": "https://github.com/facefusion/facefusion-assets",
|
|
||||||
"filename": "inswapper_128.onnx",
|
|
||||||
"url": "https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx",
|
|
||||||
"size": "555.3MB"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Deepbump",
|
"name": "Deepbump",
|
||||||
"type": "deepbump",
|
"type": "deepbump",
|
||||||
@@ -310,7 +288,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "negative_hand Negative Embedding",
|
"name": "negative_hand Negative Embedding",
|
||||||
"type": "embeddings",
|
"type": "embedding",
|
||||||
"base": "SD1.5",
|
"base": "SD1.5",
|
||||||
"save_path": "embeddings/SD1.5",
|
"save_path": "embeddings/SD1.5",
|
||||||
"description": "If you use this embedding with negatives, you can solve the issue of damaging your hands.",
|
"description": "If you use this embedding with negatives, you can solve the issue of damaging your hands.",
|
||||||
@@ -321,7 +299,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "bad_prompt Negative Embedding",
|
"name": "bad_prompt Negative Embedding",
|
||||||
"type": "embeddings",
|
"type": "embedding",
|
||||||
"base": "SD1.5",
|
"base": "SD1.5",
|
||||||
"save_path": "embeddings/SD1.5",
|
"save_path": "embeddings/SD1.5",
|
||||||
"description": "The idea behind this embedding was to somehow train the negative prompt as an embedding, thus unifying the basis of the negative prompt into one word or embedding.",
|
"description": "The idea behind this embedding was to somehow train the negative prompt as an embedding, thus unifying the basis of the negative prompt into one word or embedding.",
|
||||||
@@ -332,7 +310,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Deep Negative V1.75",
|
"name": "Deep Negative V1.75",
|
||||||
"type": "embeddings",
|
"type": "embedding",
|
||||||
"base": "SD1.5",
|
"base": "SD1.5",
|
||||||
"save_path": "embeddings/SD1.5",
|
"save_path": "embeddings/SD1.5",
|
||||||
"description": "These embedding learn what disgusting compositions and color patterns are, including faulty human anatomy, offensive color schemes, upside-down spatial structures, and more. Placing it in the negative can go a long way to avoiding these things.",
|
"description": "These embedding learn what disgusting compositions and color patterns are, including faulty human anatomy, offensive color schemes, upside-down spatial structures, and more. Placing it in the negative can go a long way to avoiding these things.",
|
||||||
@@ -343,7 +321,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "EasyNegative",
|
"name": "EasyNegative",
|
||||||
"type": "embeddings",
|
"type": "embedding",
|
||||||
"base": "SD1.5",
|
"base": "SD1.5",
|
||||||
"save_path": "embeddings/SD1.5",
|
"save_path": "embeddings/SD1.5",
|
||||||
"description": "This embedding should be used in your NEGATIVE prompt. Adjust the strength as desired (seems to scale well without any distortions), the strength required may vary based on positive and negative prompts.",
|
"description": "This embedding should be used in your NEGATIVE prompt. Adjust the strength as desired (seems to scale well without any distortions), the strength required may vary based on positive and negative prompts.",
|
||||||
@@ -488,7 +466,7 @@
|
|||||||
"name": "stabilityai/Stable Cascade: text_encoder (CLIP)",
|
"name": "stabilityai/Stable Cascade: text_encoder (CLIP)",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "Stable Cascade",
|
"base": "Stable Cascade",
|
||||||
"save_path": "clip/Stable-Cascade",
|
"save_path": "text_encoders/Stable-Cascade",
|
||||||
"description": "Stable Cascade: text_encoder",
|
"description": "Stable Cascade: text_encoder",
|
||||||
"reference": "https://huggingface.co/stabilityai/stable-cascade",
|
"reference": "https://huggingface.co/stabilityai/stable-cascade",
|
||||||
"filename": "model.safetensors",
|
"filename": "model.safetensors",
|
||||||
@@ -771,8 +749,8 @@
|
|||||||
"save_path": "loras/HyperSD/SDXL",
|
"save_path": "loras/HyperSD/SDXL",
|
||||||
"description": "Hyper-SD LoRA (4steps) - SDXL",
|
"description": "Hyper-SD LoRA (4steps) - SDXL",
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
||||||
"filename": "Hyper-SD15-4steps-lora.safetensors",
|
"filename": "Hyper-SDXL-4steps-lora.safetensors",
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-4steps-lora.safetensors",
|
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SDXL-4steps-lora.safetensors",
|
||||||
"size": "787MB"
|
"size": "787MB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -877,11 +855,67 @@
|
|||||||
"size": "5.16GB"
|
"size": "5.16GB"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "comfyanonymous/cosmos_cv8x8x8_1.0.safetensors",
|
||||||
|
"type": "VAE",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "default",
|
||||||
|
"description": "VAE model for Cosmos 1.0",
|
||||||
|
"reference": "https://huggingface.co/comfyanonymous/cosmos_1.0_text_encoder_and_VAE_ComfyUI/tree/main",
|
||||||
|
"filename": "cosmos_cv8x8x8_1.0.safetensors",
|
||||||
|
"url": "https://huggingface.co/comfyanonymous/cosmos_1.0_text_encoder_and_VAE_ComfyUI/resolve/main/vae/cosmos_cv8x8x8_1.0.safetensors",
|
||||||
|
"size": "211MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mcmonkey/Cosmos-1_0-Diffusion-7B-Text2World.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "diffusion_models/cosmos-1.0",
|
||||||
|
"description": "Cosmos 1.0 Text2World Diffusion Model (7B)",
|
||||||
|
"reference": "https://huggingface.co/mcmonkey/cosmos-1.0",
|
||||||
|
"filename": "Cosmos-1_0-Diffusion-7B-Text2World.safetensors",
|
||||||
|
"url": "https://huggingface.co/mcmonkey/cosmos-1.0/resolve/main/Cosmos-1_0-Diffusion-7B-Text2World.safetensors",
|
||||||
|
"size": "14.5GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mcmonkey/Cosmos-1_0-Diffusion-7B-Video2World.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "diffusion_models/cosmos-1.0",
|
||||||
|
"description": "Cosmos 1.0 Video2World Diffusion Model (7B)",
|
||||||
|
"reference": "https://huggingface.co/mcmonkey/cosmos-1.0",
|
||||||
|
"filename": "Cosmos-1_0-Diffusion-7B-Video2World.safetensors",
|
||||||
|
"url": "https://huggingface.co/mcmonkey/cosmos-1.0/resolve/main/Cosmos-1_0-Diffusion-7B-Video2World.safetensors",
|
||||||
|
"size": "14.5GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mcmonkey/Cosmos-1_0-Diffusion-14B-Text2World.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "diffusion_models/cosmos-1.0",
|
||||||
|
"description": "Cosmos 1.0 Text2World Diffusion Model (14B)",
|
||||||
|
"reference": "https://huggingface.co/mcmonkey/cosmos-1.0",
|
||||||
|
"filename": "Cosmos-1_0-Diffusion-14B-Text2World.safetensors",
|
||||||
|
"url": "https://huggingface.co/mcmonkey/cosmos-1.0/resolve/main/Cosmos-1_0-Diffusion-14B-Text2World.safetensors",
|
||||||
|
"size": "28.5GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mcmonkey/Cosmos-1_0-Diffusion-14B-Video2World.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "diffusion_models/cosmos-1.0",
|
||||||
|
"description": "Cosmos 1.0 Video2World Diffusion Model (14B)",
|
||||||
|
"reference": "https://huggingface.co/mcmonkey/cosmos-1.0",
|
||||||
|
"filename": "Cosmos-1_0-Diffusion-14B-Video2World.safetensors",
|
||||||
|
"url": "https://huggingface.co/mcmonkey/cosmos-1.0/resolve/main/Cosmos-1_0-Diffusion-14B-Video2World.safetensors",
|
||||||
|
"size": "28.5GB"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "google-t5/t5-base",
|
"name": "google-t5/t5-base",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5-base",
|
"base": "t5-base",
|
||||||
"save_path": "clip/t5-base",
|
"save_path": "text_encoders/t5-base",
|
||||||
"description": "T5 Base: Text-To-Text Transfer Transformer. This model can be loaded via CLIPLoader for Stable Audio workflow.",
|
"description": "T5 Base: Text-To-Text Transfer Transformer. This model can be loaded via CLIPLoader for Stable Audio workflow.",
|
||||||
"reference": "https://huggingface.co/google-t5/t5-base",
|
"reference": "https://huggingface.co/google-t5/t5-base",
|
||||||
"filename": "model.safetensors",
|
"filename": "model.safetensors",
|
||||||
@@ -892,7 +926,7 @@
|
|||||||
"name": "google-t5/t5-v1_1-xxl_encoderonly-fp16",
|
"name": "google-t5/t5-v1_1-xxl_encoderonly-fp16",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "The encoder part of https://huggingface.co/google/t5-v1_1-xxl, used with SD3 and Flux1",
|
"description": "The encoder part of https://huggingface.co/google/t5-v1_1-xxl, used with SD3 and Flux1",
|
||||||
"reference": "https://huggingface.co/mcmonkey/google_t5-v1_1-xxl_encoderonly",
|
"reference": "https://huggingface.co/mcmonkey/google_t5-v1_1-xxl_encoderonly",
|
||||||
"filename": "google_t5-v1_1-xxl_encoderonly-fp16.safetensors",
|
"filename": "google_t5-v1_1-xxl_encoderonly-fp16.safetensors",
|
||||||
@@ -903,7 +937,7 @@
|
|||||||
"name": "google-t5/t5-v1_1-xxl_encoderonly-fp8_e4m3fn",
|
"name": "google-t5/t5-v1_1-xxl_encoderonly-fp8_e4m3fn",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "The encoder part of https://huggingface.co/google/t5-v1_1-xxl, used with SD3 and Flux1",
|
"description": "The encoder part of https://huggingface.co/google/t5-v1_1-xxl, used with SD3 and Flux1",
|
||||||
"reference": "https://huggingface.co/mcmonkey/google_t5-v1_1-xxl_encoderonly",
|
"reference": "https://huggingface.co/mcmonkey/google_t5-v1_1-xxl_encoderonly",
|
||||||
"filename": "google_t5-v1_1-xxl_encoderonly-fp8_e4m3fn.safetensors",
|
"filename": "google_t5-v1_1-xxl_encoderonly-fp8_e4m3fn.safetensors",
|
||||||
@@ -916,7 +950,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-Q3_K_L.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-Q3_K_L.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (Q3_K_L quantized)",
|
"description": "t5xxl Text Encoder GGUF model. (Q3_K_L quantized)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-Q3_K_L.gguf",
|
"filename": "t5-v1_1-xxl-encoder-Q3_K_L.gguf",
|
||||||
@@ -927,7 +961,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-Q3_K_M.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-Q3_K_M.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (Q3_K_M quantized)",
|
"description": "t5xxl Text Encoder GGUF model. (Q3_K_M quantized)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-Q3_K_M.gguf",
|
"filename": "t5-v1_1-xxl-encoder-Q3_K_M.gguf",
|
||||||
@@ -938,7 +972,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-Q3_K_S.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-Q3_K_S.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (Q3_K_S quantized)",
|
"description": "t5xxl Text Encoder GGUF model. (Q3_K_S quantized)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-Q3_K_S.gguf",
|
"filename": "t5-v1_1-xxl-encoder-Q3_K_S.gguf",
|
||||||
@@ -949,7 +983,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-Q4_K_M.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-Q4_K_M.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (Q4_K_M quantized)",
|
"description": "t5xxl Text Encoder GGUF model. (Q4_K_M quantized)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-Q4_K_M.gguf",
|
"filename": "t5-v1_1-xxl-encoder-Q4_K_M.gguf",
|
||||||
@@ -960,7 +994,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-Q4_K_S.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-Q4_K_S.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (Q4_K_S quantized)",
|
"description": "t5xxl Text Encoder GGUF model. (Q4_K_S quantized)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-Q4_K_S.gguf",
|
"filename": "t5-v1_1-xxl-encoder-Q4_K_S.gguf",
|
||||||
@@ -971,7 +1005,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-Q5_K_M.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-Q5_K_M.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (Q5_K_M quantized)",
|
"description": "t5xxl Text Encoder GGUF model. (Q5_K_M quantized)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-Q5_K_M.gguf",
|
"filename": "t5-v1_1-xxl-encoder-Q5_K_M.gguf",
|
||||||
@@ -982,7 +1016,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-Q5_K_S.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-Q5_K_S.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (Q5_K_S quantized)",
|
"description": "t5xxl Text Encoder GGUF model. (Q5_K_S quantized)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-Q5_K_S.gguf",
|
"filename": "t5-v1_1-xxl-encoder-Q5_K_S.gguf",
|
||||||
@@ -993,7 +1027,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-Q6_K.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-Q6_K.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (Q6_K quantized)",
|
"description": "t5xxl Text Encoder GGUF model. (Q6_K quantized)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-Q6_K.gguf",
|
"filename": "t5-v1_1-xxl-encoder-Q6_K.gguf",
|
||||||
@@ -1004,7 +1038,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-Q8_0.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-Q8_0.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (Q8_0 quantized)",
|
"description": "t5xxl Text Encoder GGUF model. (Q8_0 quantized)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-Q8_0.gguf",
|
"filename": "t5-v1_1-xxl-encoder-Q8_0.gguf",
|
||||||
@@ -1015,7 +1049,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-f16.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-f16.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (float 16)",
|
"description": "t5xxl Text Encoder GGUF model. (float 16)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-f16.gguf",
|
"filename": "t5-v1_1-xxl-encoder-f16.gguf",
|
||||||
@@ -1026,7 +1060,7 @@
|
|||||||
"name": "city96/t5-v1_1-xxl-encoder-f32.gguf",
|
"name": "city96/t5-v1_1-xxl-encoder-f32.gguf",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "t5",
|
"base": "t5",
|
||||||
"save_path": "clip/t5",
|
"save_path": "text_encoders/t5",
|
||||||
"description": "t5xxl Text Encoder GGUF model. (float 32)",
|
"description": "t5xxl Text Encoder GGUF model. (float 32)",
|
||||||
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
"reference": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf",
|
||||||
"filename": "t5-v1_1-xxl-encoder-f32.gguf",
|
"filename": "t5-v1_1-xxl-encoder-f32.gguf",
|
||||||
@@ -1034,18 +1068,28 @@
|
|||||||
"size": "19.1GB"
|
"size": "19.1GB"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "comfyanonymous/clip_l",
|
"name": "Comfy-Org/clip_l",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "clip",
|
"base": "clip",
|
||||||
"save_path": "default",
|
"save_path": "default",
|
||||||
"description": "clip_l model",
|
"description": "clip_l model (for SD1.x, SD2.x, SDXL, SD3.5, FLUX.1, HunyuanVideo, ...) ",
|
||||||
"reference": "https://huggingface.co/comfyanonymous/flux_text_encoders/tree/main",
|
"reference": "https://huggingface.co/Comfy-Org/stable-diffusion-3.5-fp8",
|
||||||
"filename": "clip_l.safetensors",
|
"filename": "clip_l.safetensors",
|
||||||
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors",
|
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-3.5-fp8/resolve/main/text_encoders/clip_l.safetensors",
|
||||||
"size": "246MB"
|
"size": "246MB"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/clip_g",
|
||||||
|
"type": "clip",
|
||||||
|
"base": "clip",
|
||||||
|
"save_path": "default",
|
||||||
|
"description": "clip_g model (for SDXL, SD3.5)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/stable-diffusion-3.5-fp8",
|
||||||
|
"filename": "clip_g.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-3.5-fp8/resolve/main/text_encoders/clip_g.safetensors",
|
||||||
|
"size": "1.39GB"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "v1-5-pruned-emaonly.ckpt",
|
"name": "v1-5-pruned-emaonly.ckpt",
|
||||||
@@ -1665,17 +1709,6 @@
|
|||||||
"url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors",
|
"url": "https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors",
|
||||||
"size": "774.5MB"
|
"size": "774.5MB"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "kohya-ss/ControlNet-LLLite: SDXL Canny Anime",
|
|
||||||
"type": "controlnet",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "custom_nodes/ControlNet-LLLite-ComfyUI/models",
|
|
||||||
"description": "An extremely compactly designed controlnet model (a.k.a. ControlNet-LLLite). Note: The model structure is highly experimental and may be subject to change in the future.",
|
|
||||||
"reference": "https://huggingface.co/kohya-ss/controlnet-lllite",
|
|
||||||
"filename": "controllllite_v01032064e_sdxl_canny_anime.safetensors",
|
|
||||||
"url": "https://huggingface.co/kohya-ss/controlnet-lllite/resolve/main/controllllite_v01032064e_sdxl_canny_anime.safetensors",
|
|
||||||
"size": "46.2MB"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "SDXL-controlnet: OpenPose (v2)",
|
"name": "SDXL-controlnet: OpenPose (v2)",
|
||||||
"type": "controlnet",
|
"type": "controlnet",
|
||||||
@@ -2259,6 +2292,18 @@
|
|||||||
"url": "https://huggingface.co/CiaraRowles/TemporalDiff/resolve/main/temporaldiff-v1-animatediff.ckpt",
|
"url": "https://huggingface.co/CiaraRowles/TemporalDiff/resolve/main/temporaldiff-v1-animatediff.ckpt",
|
||||||
"size": "1.67GB"
|
"size": "1.67GB"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Leoxing/pia.ckpt",
|
||||||
|
"type": "animatediff-pia",
|
||||||
|
"base": "SD1.x",
|
||||||
|
"save_path": "animatediff_models",
|
||||||
|
"description": "AnimateDiff-PIA Model",
|
||||||
|
"reference": "https://huggingface.co/Leoxing/PIA/tree/main",
|
||||||
|
"filename": "pia.ckpt",
|
||||||
|
"url": "https://huggingface.co/Leoxing/PIA/resolve/main/pia.ckpt",
|
||||||
|
"size": "1.67GB"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "animatediff/v2_lora_PanLeft.ckpt (ComfyUI-AnimateDiff-Evolved) (Updated path)",
|
"name": "animatediff/v2_lora_PanLeft.ckpt (ComfyUI-AnimateDiff-Evolved) (Updated path)",
|
||||||
"type": "motion lora",
|
"type": "motion lora",
|
||||||
@@ -2746,39 +2791,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "pfg-novel-n10.pt",
|
|
||||||
"type": "PFG",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "custom_nodes/pfg-ComfyUI/models",
|
|
||||||
"description": "Pressing 'install' directly downloads the model from the pfg-ComfyUI/models extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
|
|
||||||
"reference": "https://huggingface.co/furusu/PFG",
|
|
||||||
"filename": "pfg-novel-n10.pt",
|
|
||||||
"url": "https://huggingface.co/furusu/PFG/resolve/main/pfg-novel-n10.pt",
|
|
||||||
"size": "23.6MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pfg-wd14-n10.pt",
|
|
||||||
"type": "PFG",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "custom_nodes/pfg-ComfyUI/models",
|
|
||||||
"description": "Pressing 'install' directly downloads the model from the pfg-ComfyUI/models extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
|
|
||||||
"reference": "https://huggingface.co/furusu/PFG",
|
|
||||||
"filename": "pfg-wd14-n10.pt",
|
|
||||||
"url": "https://huggingface.co/furusu/PFG/resolve/main/pfg-wd14-n10.pt",
|
|
||||||
"size": "31.5MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pfg-wd15beta2-n10.pt",
|
|
||||||
"type": "PFG",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "custom_nodes/pfg-ComfyUI/models",
|
|
||||||
"description": "Pressing 'install' directly downloads the model from the pfg-ComfyUI/models extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
|
|
||||||
"reference": "https://huggingface.co/furusu/PFG",
|
|
||||||
"filename": "pfg-wd15beta2-n10.pt",
|
|
||||||
"url": "https://huggingface.co/furusu/PFG/resolve/main/pfg-wd15beta2-n10.pt",
|
|
||||||
"size": "31.5MB"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "GFPGANv1.4.pth",
|
"name": "GFPGANv1.4.pth",
|
||||||
"type": "GFPGAN",
|
"type": "GFPGAN",
|
||||||
@@ -2944,50 +2956,6 @@
|
|||||||
"url": "https://huggingface.co/InstantX/InstantID/resolve/main/ControlNetModel/diffusion_pytorch_model.safetensors",
|
"url": "https://huggingface.co/InstantX/InstantID/resolve/main/ControlNetModel/diffusion_pytorch_model.safetensors",
|
||||||
"size": "2.50GB"
|
"size": "2.50GB"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "efficient_sam_s_cpu.jit [ComfyUI-YoloWorld-EfficientSAM]",
|
|
||||||
"type": "efficient_sam",
|
|
||||||
"base": "efficient_sam",
|
|
||||||
"save_path": "custom_nodes/ComfyUI-YoloWorld-EfficientSAM",
|
|
||||||
"description": "Install efficient_sam_s_cpu.jit into ComfyUI-YoloWorld-EfficientSAM",
|
|
||||||
"reference": "https://huggingface.co/camenduru/YoloWorld-EfficientSAM/tree/main",
|
|
||||||
"filename": "efficient_sam_s_cpu.jit",
|
|
||||||
"url": "https://huggingface.co/camenduru/YoloWorld-EfficientSAM/resolve/main/efficient_sam_s_cpu.jit",
|
|
||||||
"size": "106.0MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "efficient_sam_s_gpu.jit [ComfyUI-YoloWorld-EfficientSAM]",
|
|
||||||
"type": "efficient_sam",
|
|
||||||
"base": "efficient_sam",
|
|
||||||
"save_path": "custom_nodes/ComfyUI-YoloWorld-EfficientSAM",
|
|
||||||
"description": "Install efficient_sam_s_gpu.jit into ComfyUI-YoloWorld-EfficientSAM",
|
|
||||||
"reference": "https://huggingface.co/camenduru/YoloWorld-EfficientSAM/tree/main",
|
|
||||||
"filename": "efficient_sam_s_gpu.jit",
|
|
||||||
"url": "https://huggingface.co/camenduru/YoloWorld-EfficientSAM/resolve/main/efficient_sam_s_gpu.jit",
|
|
||||||
"size": "106.0MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "shape_predictor_68_face_landmarks.dat [Face Analysis]",
|
|
||||||
"type": "Shape Predictor",
|
|
||||||
"base": "DLIB",
|
|
||||||
"save_path": "custom_nodes/ComfyUI_FaceAnalysis/dlib",
|
|
||||||
"description": "To use the Face Analysis for ComfyUI custom node, installation of this model is needed.",
|
|
||||||
"reference": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/tree/main",
|
|
||||||
"filename": "shape_predictor_68_face_landmarks.dat",
|
|
||||||
"url": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/resolve/main/shape_predictor_68_face_landmarks.dat",
|
|
||||||
"size": "99.7MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dlib_face_recognition_resnet_model_v1.dat [Face Analysis]",
|
|
||||||
"type": "Face Recognition",
|
|
||||||
"base": "DLIB",
|
|
||||||
"save_path": "custom_nodes/ComfyUI_FaceAnalysis/dlib",
|
|
||||||
"description": "To use the Face Analysis for ComfyUI custom node, installation of this model is needed.",
|
|
||||||
"reference": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/tree/main",
|
|
||||||
"filename": "dlib_face_recognition_resnet_model_v1.dat",
|
|
||||||
"url": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/resolve/main/dlib_face_recognition_resnet_model_v1.dat",
|
|
||||||
"size": "22.5MB"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "InstanceDiffusion/fusers",
|
"name": "InstanceDiffusion/fusers",
|
||||||
"type": "InstanceDiffusion",
|
"type": "InstanceDiffusion",
|
||||||
@@ -3362,50 +3330,6 @@
|
|||||||
"url": "https://huggingface.co/lllyasviel/ic-light/resolve/main/iclight_sd15_fcon.safetensors",
|
"url": "https://huggingface.co/lllyasviel/ic-light/resolve/main/iclight_sd15_fcon.safetensors",
|
||||||
"size": "1.72GB"
|
"size": "1.72GB"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "ID-Animator/animator.ckpt",
|
|
||||||
"type": "ID-Animator",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "custom_nodes/ComfyUI_ID_Animator/models",
|
|
||||||
"description": "ID-Animator checkpoint",
|
|
||||||
"reference": "https://huggingface.co/spaces/ID-Animator/ID-Animator",
|
|
||||||
"filename": "animator.ckpt",
|
|
||||||
"url": "https://huggingface.co/spaces/ID-Animator/ID-Animator/resolve/main/animator.ckpt",
|
|
||||||
"size": "247.3MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ID-Animator/mm_sd_v15_v2.ckpt",
|
|
||||||
"type": "ID-Animator",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "custom_nodes/ComfyUI_ID_Animator/models/animatediff_models",
|
|
||||||
"description": "AnimateDiff checkpoint for ID-Animator",
|
|
||||||
"reference": "https://huggingface.co/spaces/ID-Animator/ID-Animator",
|
|
||||||
"filename": "mm_sd_v15_v2.ckpt",
|
|
||||||
"url": "https://huggingface.co/spaces/ID-Animator/ID-Animator/resolve/main/mm_sd_v15_v2.ckpt",
|
|
||||||
"size": "1.82GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ID-Animator/image_encoder",
|
|
||||||
"type": "ID-Animator",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "custom_nodes/ComfyUI_ID_Animator/models/image_encoder",
|
|
||||||
"description": "CLIP Image encoder for ID-Animator",
|
|
||||||
"reference": "https://huggingface.co/spaces/ID-Animator/ID-Animator",
|
|
||||||
"filename": "model.safetensors",
|
|
||||||
"url": "https://huggingface.co/spaces/ID-Animator/ID-Animator/resolve/main/image_encoder/model.safetensors",
|
|
||||||
"size": "2.53GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "TencentARC/CustomNet",
|
|
||||||
"type": "CustomNet",
|
|
||||||
"base": "CustomNet",
|
|
||||||
"save_path": "custom_nodes/ComfyUI_CustomNet/pretrain",
|
|
||||||
"description": "CustomNet pretrained model for ComfyUI_CustomNet",
|
|
||||||
"reference": "https://huggingface.co/TencentARC/CustomNet/tree/main",
|
|
||||||
"filename": "customnet_v1.pt",
|
|
||||||
"url": "https://huggingface.co/TencentARC/CustomNet/resolve/main/customnet_v1.pt",
|
|
||||||
"size": "5.71GB"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "TTPlanet/TTPLanet_SDXL_Controlnet_Tile_Realistic v2 (fp16)",
|
"name": "TTPlanet/TTPLanet_SDXL_Controlnet_Tile_Realistic v2 (fp16)",
|
||||||
"type": "controlnet",
|
"type": "controlnet",
|
||||||
@@ -3494,17 +3418,6 @@
|
|||||||
"url": "https://huggingface.co/ViperYX/RGT/resolve/main/RGT_S/RGT_S_x4.pth",
|
"url": "https://huggingface.co/ViperYX/RGT/resolve/main/RGT_S/RGT_S_x4.pth",
|
||||||
"size": "136.0MB"
|
"size": "136.0MB"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Doubiiu/ToonCrafter model checkpoint",
|
|
||||||
"type": "checkpoint",
|
|
||||||
"base": "ToonCrafter",
|
|
||||||
"save_path": "custom_nodes/ComfyUI-ToonCrafter/ToonCrafter/checkpoints/tooncrafter_512_interp_v1",
|
|
||||||
"description": "ToonCrafter checkpoint model for ComfyUI-ToonCrafter",
|
|
||||||
"reference": "https://huggingface.co/Doubiiu/ToonCrafter/tree/main",
|
|
||||||
"filename": "model.ckpt",
|
|
||||||
"url": "https://huggingface.co/Doubiiu/ToonCrafter/resolve/main/model.ckpt",
|
|
||||||
"size": "10.5GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "InstantX/FLUX.1-dev Controlnet (Union)",
|
"name": "InstantX/FLUX.1-dev Controlnet (Union)",
|
||||||
@@ -4047,6 +3960,17 @@
|
|||||||
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/vae/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"
|
"size": "493MB"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/hunyuan_video_image_to_video_720p_bf16.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Hunyuan Video",
|
||||||
|
"save_path": "diffusion_models/hunyuan_video",
|
||||||
|
"description": "Huyuan Video Image2Video diffusion model. repackaged version.",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged",
|
||||||
|
"filename": "hunyuan_video_image_to_video_720p_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/diffusion_models/hunyuan_video_image_to_video_720p_bf16.safetensors",
|
||||||
|
"size": "25.6GB"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Comfy-Org/llava_llama3_fp8_scaled.safetensors",
|
"name": "Comfy-Org/llava_llama3_fp8_scaled.safetensors",
|
||||||
@@ -4070,6 +3994,17 @@
|
|||||||
"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": "Comfy-Org/llava_llama3_vision.safetensors",
|
||||||
|
"type": "clip_vision",
|
||||||
|
"base": "LLaVA-Llama-3",
|
||||||
|
"save_path": "text_encoders",
|
||||||
|
"description": "llava_llama3_vision clip vison model. This is required for using Hunyuan Video Image2Video.",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged",
|
||||||
|
"filename": "llava_llama3_vision.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/clip_vision/llava_llama3_vision.safetensors",
|
||||||
|
"size": "649MB"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "FLUX.1 [Schnell] Diffusion model",
|
"name": "FLUX.1 [Schnell] Diffusion model",
|
||||||
@@ -4397,23 +4332,11 @@
|
|||||||
"size": "12.7GB"
|
"size": "12.7GB"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
"name": "BAAI/SegGPT",
|
|
||||||
"type": "SegGPT",
|
|
||||||
"base": "SegGPT",
|
|
||||||
"save_path": "custom_nodes/comfyui-SegGPT",
|
|
||||||
"description": "SegGPT",
|
|
||||||
"reference": "https://huggingface.co/BAAI/SegGPT",
|
|
||||||
"filename": "seggpt_vit_large.pth",
|
|
||||||
"url": "https://huggingface.co/BAAI/SegGPT/resolve/main/seggpt_vit_large.pth",
|
|
||||||
"size": "1.48GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors [Long CLIP L]",
|
"name": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors [Long CLIP L]",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "clip",
|
"base": "clip",
|
||||||
"save_path": "clip/long_clip",
|
"save_path": "text_encoders/long_clip",
|
||||||
"description": "Greatly improved TEXT + Detail (as CLIP-L for Flux.1)",
|
"description": "Greatly improved TEXT + Detail (as CLIP-L for Flux.1)",
|
||||||
"reference": "https://huggingface.co/zer0int",
|
"reference": "https://huggingface.co/zer0int",
|
||||||
"filename": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors",
|
"filename": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors",
|
||||||
@@ -4424,7 +4347,7 @@
|
|||||||
"name": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors [Long CLIP L]",
|
"name": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors [Long CLIP L]",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
"base": "clip",
|
"base": "clip",
|
||||||
"save_path": "clip/long_clip",
|
"save_path": "text_encoders/long_clip",
|
||||||
"description": "Greatly improved TEXT + Detail (as CLIP-L for Flux.1)",
|
"description": "Greatly improved TEXT + Detail (as CLIP-L for Flux.1)",
|
||||||
"reference": "https://huggingface.co/zer0int",
|
"reference": "https://huggingface.co/zer0int",
|
||||||
"filename": "ViT-L-14-TEXT-detail-improved-hiT-GmP-TE-only-HF.safetensors",
|
"filename": "ViT-L-14-TEXT-detail-improved-hiT-GmP-TE-only-HF.safetensors",
|
||||||
@@ -4646,6 +4569,17 @@
|
|||||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/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"
|
"size": "5.72GB"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 2B v0.9.5 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.5.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.5.safetensors",
|
||||||
|
"size": "6.34GB"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "XLabs-AI/flux-canny-controlnet-v3.safetensors",
|
"name": "XLabs-AI/flux-canny-controlnet-v3.safetensors",
|
||||||
@@ -4725,6 +4659,401 @@
|
|||||||
"filename": "ip_adapter.safetensors",
|
"filename": "ip_adapter.safetensors",
|
||||||
"url": "https://huggingface.co/XLabs-AI/flux-ip-adapter/resolve/main/ip_adapter.safetensors",
|
"url": "https://huggingface.co/XLabs-AI/flux-ip-adapter/resolve/main/ip_adapter.safetensors",
|
||||||
"size": "982MB"
|
"size": "982MB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "efficient_sam_s_cpu.jit [ComfyUI-YoloWorld-EfficientSAM]",
|
||||||
|
"type": "efficient_sam",
|
||||||
|
"base": "efficient_sam",
|
||||||
|
"save_path": "yolo_world",
|
||||||
|
"description": "Install efficient_sam_s_cpu.jit into ComfyUI-YoloWorld-EfficientSAM",
|
||||||
|
"reference": "https://huggingface.co/camenduru/YoloWorld-EfficientSAM/tree/main",
|
||||||
|
"filename": "efficient_sam_s_cpu.jit",
|
||||||
|
"url": "https://huggingface.co/camenduru/YoloWorld-EfficientSAM/resolve/main/efficient_sam_s_cpu.jit",
|
||||||
|
"size": "106.0MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "efficient_sam_s_gpu.jit [ComfyUI-YoloWorld-EfficientSAM]",
|
||||||
|
"type": "efficient_sam",
|
||||||
|
"base": "efficient_sam",
|
||||||
|
"save_path": "yolo_world",
|
||||||
|
"description": "Install efficient_sam_s_gpu.jit into ComfyUI-YoloWorld-EfficientSAM",
|
||||||
|
"reference": "https://huggingface.co/camenduru/YoloWorld-EfficientSAM/tree/main",
|
||||||
|
"filename": "efficient_sam_s_gpu.jit",
|
||||||
|
"url": "https://huggingface.co/camenduru/YoloWorld-EfficientSAM/resolve/main/efficient_sam_s_gpu.jit",
|
||||||
|
"size": "106.0MB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "TencentARC/CustomNet V1",
|
||||||
|
"type": "CustomNet",
|
||||||
|
"base": "CustomNet",
|
||||||
|
"save_path": "checkpoints/customnet",
|
||||||
|
"description": "CustomNet pretrained model for ComfyUI_CustomNet",
|
||||||
|
"reference": "https://huggingface.co/TencentARC/CustomNet/tree/main",
|
||||||
|
"filename": "customnet_v1.pt",
|
||||||
|
"url": "https://huggingface.co/TencentARC/CustomNet/resolve/main/customnet_v1.pt",
|
||||||
|
"size": "5.71GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TencentARC/CustomNet Inpaint V1",
|
||||||
|
"type": "CustomNet",
|
||||||
|
"base": "CustomNet",
|
||||||
|
"save_path": "checkpoints/customnet",
|
||||||
|
"description": "CustomNet Inpaint pretrained model for ComfyUI_CustomNet",
|
||||||
|
"reference": "https://huggingface.co/TencentARC/CustomNet/tree/main",
|
||||||
|
"filename": "customnet_inpaint_v1.pt",
|
||||||
|
"url": "https://huggingface.co/TencentARC/CustomNet/resolve/main/customnet_inpaint_v1.pt",
|
||||||
|
"size": "5.71GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "deepseek-ai/Janus-Pro-1B",
|
||||||
|
"type": "Janus-Pro",
|
||||||
|
"base": "Janus-Pro",
|
||||||
|
"save_path": "Janus-Pro",
|
||||||
|
"description": "[SNAPSHOT] Janus-Pro-1B model.[w/You cannot download this item on ComfyUI-Manager versions below V3.18]",
|
||||||
|
"reference": "https://huggingface.co/deepseek-ai/Janus-Pro-1B",
|
||||||
|
"filename": "<huggingface>",
|
||||||
|
"url": "deepseek-ai/Janus-Pro-1B",
|
||||||
|
"size": "7.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deepseek-ai/Janus-Pro-7B",
|
||||||
|
"type": "Janus-Pro",
|
||||||
|
"base": "Janus-Pro",
|
||||||
|
"save_path": "Janus-Pro",
|
||||||
|
"description": "[SNAPSHOT] Janus-Pro-7B model.[w/You cannot download this item on ComfyUI-Manager versions below V3.18]",
|
||||||
|
"reference": "https://huggingface.co/deepseek-ai/Janus-Pro-7B",
|
||||||
|
"filename": "<huggingface>",
|
||||||
|
"url": "deepseek-ai/Janus-Pro-7B",
|
||||||
|
"size": "14.85GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kolors/vae/diffusion_pytorch_model.fp16.safetensors",
|
||||||
|
"type": "VAE",
|
||||||
|
"base": "Kolors",
|
||||||
|
"save_path": "vae/kolors",
|
||||||
|
"description": "Kolors VAE",
|
||||||
|
"reference": "https://huggingface.co/Kwai-Kolors/Kolors",
|
||||||
|
"filename": "diffusion_pytorch_model.fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Kwai-Kolors/Kolors/resolve/main/vae/diffusion_pytorch_model.fp16.safetensors",
|
||||||
|
"size": "167MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kolors/vae/diffusion_pytorch_model.safetensors",
|
||||||
|
"type": "VAE",
|
||||||
|
"base": "Kolors",
|
||||||
|
"save_path": "vae/kolors",
|
||||||
|
"description": "Kolors VAE",
|
||||||
|
"reference": "https://huggingface.co/Kwai-Kolors/Kolors",
|
||||||
|
"filename": "diffusion_pytorch_model.safetensors",
|
||||||
|
"url": "https://huggingface.co/Kwai-Kolors/Kolors/resolve/main/vae/diffusion_pytorch_model.safetensors",
|
||||||
|
"size": "335MB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 480p 14B (bf16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 480p 14B (bf16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_480p_14B_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_480p_14B_bf16.safetensors",
|
||||||
|
"size": "32.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 480p 14B (fp16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 480p 14B (fp16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_480p_14B_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_480p_14B_fp16.safetensors",
|
||||||
|
"size": "32.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 480p 14B (fp8_e4m3fn)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 480p 14B (fp8_e4m3fn)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_480p_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_480p_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"size": "16.4GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 480p 14B (fp8_scaled)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 480p 14B (fp8_scaled)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_480p_14B_fp8_scaled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_480p_14B_fp8_scaled.safetensors",
|
||||||
|
"size": "16.4GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 720p 14B (bf16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 720p 14B (bf16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_720p_14B_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_720p_14B_bf16.safetensors",
|
||||||
|
"size": "32.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 720p 14B (fp16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 720p 14B (fp16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_720p_14B_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_720p_14B_fp16.safetensors",
|
||||||
|
"size": "32.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 720p 14B (fp8_e4m3fn)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 720p 14B (fp8_e4m3fn)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_720p_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_720p_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"size": "16.4GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 720p 14B (fp8_scaled)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 720p 14B (fp8_scaled)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_720p_14B_fp8_scaled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_720p_14B_fp8_scaled.safetensors",
|
||||||
|
"size": "16.4GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 1.3B (bf16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 1.3B (bf16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_1.3B_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_1.3B_bf16.safetensors",
|
||||||
|
"size": "2.84GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 1.3B (fp16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 1.3B (fp16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_1.3B_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_1.3B_fp16.safetensors",
|
||||||
|
"size": "2.84GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 14B (bf16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 14B (bf16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_14B_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_14B_bf16.safetensors",
|
||||||
|
"size": "28.6GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 14B (fp16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 14B (fp16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_14B_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_14B_fp16.safetensors",
|
||||||
|
"size": "28.6GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 14B (fp8_e4m3fn)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 14B (fp8_e4m3fn)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"size": "14.3GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 14B (fp8_scaled)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 14B (fp8_scaled)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_14B_fp8_scaled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_14B_fp8_scaled.safetensors",
|
||||||
|
"size": "14.3GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 VAE",
|
||||||
|
"type": "vae",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "vae",
|
||||||
|
"description": "Wan2.1 VAE model",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan_2.1_vae.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/vae/wan_2.1_vae.safetensors",
|
||||||
|
"size": "254MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/clip_vision_h.safetensors",
|
||||||
|
"type": "clip_vision",
|
||||||
|
"base": "clip_vision_h",
|
||||||
|
"save_path": "clip_vision",
|
||||||
|
"description": "clip_vision_h model for Wan2.1",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "clip_vision_h.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/clip_vision/clip_vision_h.safetensors",
|
||||||
|
"size": "1.26GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/umt5_xxl_fp16.safetensors",
|
||||||
|
"type": "clip",
|
||||||
|
"base": "umt5_xxl",
|
||||||
|
"save_path": "text_encoders",
|
||||||
|
"description": "umt5_xxl_fp16 text encoder for Wan2.1",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "umt5_xxl_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp16.safetensors",
|
||||||
|
"size": "11.4GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||||
|
"type": "clip",
|
||||||
|
"base": "umt5_xxl",
|
||||||
|
"save_path": "text_encoders",
|
||||||
|
"description": "umt5_xxl_fp8_e4m3fn_scaled text encoder for Wan2.1",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||||
|
"size": "6.74GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "lllyasviel/FramePackI2V_HY",
|
||||||
|
"type": "FramePackI2V",
|
||||||
|
"base": "FramePackI2V",
|
||||||
|
"save_path": "diffusers/lllyasviel",
|
||||||
|
"description": "[SNAPSHOT] This is the f1k1_x_g9_f1k1f2k2f16k4_td FramePack for HY. [w/You cannot download this item on ComfyUI-Manager versions below V3.18]",
|
||||||
|
"reference": "https://huggingface.co/lllyasviel/FramePackI2V_HY",
|
||||||
|
"filename": "<huggingface>",
|
||||||
|
"url": "lllyasviel/FramePackI2V_HY",
|
||||||
|
"size": "25.75GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "LTX-Video Spatial Upscaler v0.9.7",
|
||||||
|
"type": "upscale",
|
||||||
|
"base": "upscale",
|
||||||
|
"save_path": "default",
|
||||||
|
"description": "Spatial upscaler model for LTX-Video. This model enhances the spatial resolution of generated videos.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-spatial-upscaler-0.9.7.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-spatial-upscaler-0.9.7.safetensors",
|
||||||
|
"size": "505MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video Temporal Upscaler v0.9.7",
|
||||||
|
"type": "upscale",
|
||||||
|
"base": "upscale",
|
||||||
|
"save_path": "default",
|
||||||
|
"description": "Temporal upscaler model for LTX-Video. This model enhances the temporal resolution and smoothness of generated videos.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-temporal-upscaler-0.9.7.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-temporal-upscaler-0.9.7.safetensors",
|
||||||
|
"size": "524MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "High-resolution quality LTX-Video 13B model.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-dev.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-dev.safetensors",
|
||||||
|
"size": "28.6GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B FP8 v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "Quantized version of the LTX-Video 13B model, optimized for lower VRAM usage while maintaining high quality.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-dev-fp8.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-dev-fp8.safetensors",
|
||||||
|
"size": "15.7GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B Distilled v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "Distilled version of the LTX-Video 13B model, providing improved efficiency while maintaining high-resolution quality.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-distilled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled.safetensors",
|
||||||
|
"size": "28.6GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B Distilled FP8 v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "Quantized distilled version of the LTX-Video 13B model, optimized for even lower VRAM usage while maintaining quality.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-distilled-fp8.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-fp8.safetensors",
|
||||||
|
"size": "15.7GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B Distilled LoRA v0.9.7",
|
||||||
|
"type": "lora",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "loras",
|
||||||
|
"description": "A LoRA adapter that transforms the standard LTX-Video 13B model into a distilled version when loaded.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-distilled-lora128.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-lora128.safetensors",
|
||||||
|
"size": "1.33GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Latent Bridge Matching for Image Relighting",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "LBM",
|
||||||
|
"save_path": "diffusion_models/LBM",
|
||||||
|
"description": "Latent Bridge Matching (LBM) Relighting model",
|
||||||
|
"reference": "https://huggingface.co/jasperai/LBM_relighting",
|
||||||
|
"filename": "LBM_relighting.safetensors",
|
||||||
|
"url": "https://huggingface.co/jasperai/LBM_relighting/resolve/main/model.safetensors",
|
||||||
|
"size": "5.02GB"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,25 @@
|
|||||||
{
|
{
|
||||||
"custom_nodes": [
|
"custom_nodes": [
|
||||||
|
{
|
||||||
|
"author": "SanDiegoDude",
|
||||||
|
"title": "ComfyUI-HiDream-Sampler [WIP]",
|
||||||
|
"reference": "https://github.com/SanDiegoDude/ComfyUI-HiDream-Sampler",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/SanDiegoDude/ComfyUI-HiDream-Sampler"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A collection of enhanced nodes for ComfyUI that provide powerful additional functionality to your workflows.\nNOTE: The files in the repo are not organized."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "PramaLLC",
|
||||||
|
"title": "ComfyUI BEN - Background Erase Network",
|
||||||
|
"reference": "https://github.com/PramaLLC/BEN2_ComfyUI",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/PramaLLC/BEN2_ComfyUI"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Remove backgrounds from images with [a/BEN2](https://huggingface.co/PramaLLC/BEN2) in ComfyUI\nOriginal repo: [a/https://github.com/DoctorDiffusion/ComfyUI-BEN](https://github.com/DoctorDiffusion/ComfyUI-BEN)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"author": "BlenderNeko",
|
"author": "BlenderNeko",
|
||||||
"title": "ltdrdata/ComfyUI_TiledKSampler",
|
"title": "ltdrdata/ComfyUI_TiledKSampler",
|
||||||
@@ -119,6 +139,16 @@
|
|||||||
],
|
],
|
||||||
"install_type": "git-clone",
|
"install_type": "git-clone",
|
||||||
"description": "A forked version of ComfyUI_ExtraModels. (modified by Efficient-Large-Model)"
|
"description": "A forked version of ComfyUI_ExtraModels. (modified by Efficient-Large-Model)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Pablerdo",
|
||||||
|
"title": "ComfyUI-PSNodes",
|
||||||
|
"reference": "https://github.com/Pablerdo/ComfyUI-PSNodes",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Pablerdo/ComfyUI-PSNodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A fork of KJNodes for ComfyUI.\nVarious quality of life -nodes for ComfyUI, mostly just visual stuff to improve usability"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,945 @@
|
|||||||
"install_type": "git-clone",
|
"install_type": "git-clone",
|
||||||
"description": "If you see this message, your ComfyUI-Manager is outdated.\nLegacy channel provides only the list of the deprecated nodes. If you want to find the complete node list, please go to the Default channel."
|
"description": "If you see this message, your ComfyUI-Manager is outdated.\nLegacy channel provides only the list of the deprecated nodes. If you want to find the complete node list, please go to the Default channel."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"author": "syaofox",
|
||||||
|
"title": "ComfyUI_fnodes [REMOVED]",
|
||||||
|
"reference": "https://github.com/syaofox/ComfyUI_fnodes",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/syaofox/ComfyUI_fnodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "ComfyUI_fnodes is a collection of custom nodes designed for ComfyUI. These nodes provide additional functionality that can enhance your ComfyUI workflows.\nFile manipulation tools, Image resizing tools, IPAdapter tools, Image processing tools, Mask tools, Face analysis tools, Sampler tools, Miscellaneous tools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Hangover3832",
|
||||||
|
"title": "ComfyUI-Hangover-Moondream [DEPRECATED]",
|
||||||
|
"reference": "https://github.com/Hangover3832/ComfyUI-Hangover-Moondream",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Hangover3832/ComfyUI-Hangover-Moondream"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Moondream is a lightweight multimodal large language model.\n[w/WARN:Additional python code will be downloaded from huggingface and executed. You have to trust this creator if you want to use this node!]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Hangover3832",
|
||||||
|
"title": "Recognize Anything Model (RAM) for ComfyUI [DEPRECATED]",
|
||||||
|
"reference": "https://github.com/Hangover3832/ComfyUI-Hangover-Recognize_Anything",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Hangover3832/ComfyUI-Hangover-Recognize_Anything"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This is an image recognition node for ComfyUI based on the RAM++ model from [a/xinyu1205](https://huggingface.co/xinyu1205).\nThis node outputs a string of tags with all the recognized objects and elements in the image in English or Chinese language.\nFor image tagging and captioning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Hangover3832",
|
||||||
|
"title": "ComfyUI-Hangover-Nodes [DEPRECATED]",
|
||||||
|
"reference": "https://github.com/Hangover3832/ComfyUI-Hangover-Nodes",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Hangover3832/ComfyUI-Hangover-Nodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Nodes: MS kosmos-2 Interrogator, Save Image w/o Metadata, Image Scale Bounding Box. An implementation of Microsoft [a/kosmos-2](https://huggingface.co/microsoft/kosmos-2-patch14-224) image to text transformer."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "SirLatore",
|
||||||
|
"title": "ComfyUI-IPAdapterWAN [REMOVED]",
|
||||||
|
"reference": "https://github.com/SirLatore/ComfyUI-IPAdapterWAN",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/SirLatore/ComfyUI-IPAdapterWAN"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This extension adapts the [a/InstantX IP-Adapter for SD3.5-Large](https://huggingface.co/InstantX/SD3.5-Large-IP-Adapter) to work with Wan 2.1 and other UNet-based video/image models in ComfyUI.\nUnlike the original SD3 version (which depends on joint_blocks from MMDiT), this version performs sampling-time identity conditioning by dynamically injecting into attention layers — making it compatible with models like Wan 2.1, AnimateDiff, and other non-SD3 pipelines."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Jpzz",
|
||||||
|
"title": "ComfyUI-VirtualInteraction [UNSAFE/REMOVED]",
|
||||||
|
"reference": "https://github.com/Jpzz/ComfyUI-VirtualInteraction",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Jpzz/ComfyUI-VirtualInteraction"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: virtual interaction custom node when using generative movie\n[w/This nodepack contains a node which is reading arbitrary excel file.]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "satche",
|
||||||
|
"title": "Prompt Factory [REMOVED]",
|
||||||
|
"reference": "https://github.com/satche/comfyui-prompt-factory",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/satche/comfyui-prompt-factory"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A modular system that adds randomness to prompt generation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "MITCAP",
|
||||||
|
"title": "ComfyUI OpenAI DALL-E 3 Node [REMOVED]",
|
||||||
|
"reference": "https://github.com/MITCAP/OpenAI-ComfyUI",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/MITCAP/OpenAI-ComfyUI"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This project provides custom nodes for ComfyUI that integrate with OpenAI's DALL-E 3 and GPT-4o models. The nodes allow users to generate images and describe images using OpenAI's API.\nNOTE: The files in the repo are not organized."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "raspie10032",
|
||||||
|
"title": "ComfyUI NAI Prompt Converter [REMOVED]",
|
||||||
|
"reference": "https://github.com/raspie10032/ComfyUI_RS_NAI_Local_Prompt_converter",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/raspie10032/ComfyUI_RS_NAI_Local_Prompt_converter"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A custom node extension for ComfyUI that enables conversion between different prompt formats: NovelAI V4, ComfyUI, and old NovelAI."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "holchan",
|
||||||
|
"title": "ComfyUI-ModelDownloader [REMOVED]",
|
||||||
|
"reference": "https://github.com/holchan/ComfyUI-ModelDownloader",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/holchan/ComfyUI-ModelDownloader"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A ComfyUI node to download models(Checkpoints and LoRA) from external links and act as an output standalone node."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Kur0butiMegane",
|
||||||
|
"title": "Comfyui-StringUtils [DEPRECATED]",
|
||||||
|
"reference": "https://github.com/Kur0butiMegane/Comfyui-StringUtils",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Kur0butiMegane/Comfyui-StringUtils"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Prompt Normalizer, String Splitter, String Line Selector, Extract Markup Value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Apache0ne",
|
||||||
|
"title": "ComfyUI-LantentCompose [REMOVED]",
|
||||||
|
"reference": "https://github.com/Apache0ne/ComfyUI-LantentCompose",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Apache0ne/ComfyUI-LantentCompose"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Interpolate sdxl latents using slerp with and without a mask. use with unsample nodes for best effect.\nNOTE: The files in the repo are not organized."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "jax-explorer",
|
||||||
|
"title": "ComfyUI-H-flow [REMOVED]",
|
||||||
|
"reference": "https://github.com/jax-explorer/ComfyUI-H-flow",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/jax-explorer/ComfyUI-H-flow"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Wan2-1 Image To Video, LLM Task, Save Image, Save Video, Show Text, FluxPro Ultra, IdeogramV2 Turbo, Runway Image To Video, Kling Image To Video, Replace Text, Join Text, Test Image, Test Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Apache0ne",
|
||||||
|
"title": "SambaNova [REMOVED]",
|
||||||
|
"id": "SambaNovaAPI",
|
||||||
|
"reference": "https://github.com/Apache0ne/SambaNova",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Apache0ne/SambaNova"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Super Fast LLM's llama3.1-405B,70B,8B and more"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Apache0ne",
|
||||||
|
"title": "ComfyUI-EasyUrlLoader [REMOVED]",
|
||||||
|
"id": "easy-url-loader",
|
||||||
|
"reference": "https://github.com/Apache0ne/ComfyUI-EasyUrlLoader",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Apache0ne/ComfyUI-EasyUrlLoader"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A simple YT downloader node for ComfyUI using video Urls. Can be used with VHS nodes etc."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "nxt5656",
|
||||||
|
"title": "ComfyUI-Image2OSS [REMOVED]",
|
||||||
|
"reference": "https://github.com/nxt5656/ComfyUI-Image2OSS",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/nxt5656/ComfyUI-Image2OSS"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Upload the image to Alibaba Cloud OSS."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "ainewsto",
|
||||||
|
"title": "Comfyui_Comfly",
|
||||||
|
"reference": "https://github.com/ainewsto/Comfyui_Comfly",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/ainewsto/Comfyui_Comfly"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Comfly_Mj, Comfly_mjstyle, Comfly_upload, Comfly_Mju, Comfly_Mjv, Comfly_kling_videoPreview\nNOTE: Comfyui_Comfly_v2 is introduced."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-to-inpaint",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-to-inpaint",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-to-inpaint"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Send preview image to inpaint workflow."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "magic-quill",
|
||||||
|
"title": "ComfyUI_MagicQuill [NOT MAINTAINED]",
|
||||||
|
"id": "MagicQuill",
|
||||||
|
"reference": "https://github.com/magic-quill/ComfyUI_MagicQuill",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/magic-quill/ComfyUI_MagicQuill"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Towards GPT-4 like large language and visual assistant.\nNOTE: The current version has not been maintained for a long time and does not work. Please use https://github.com/brantje/ComfyUI_MagicQuill instead."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-event-handler [USAFE/REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-event-handler",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-event-handler"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Javascript code will run when an event fires. [w/This node allows you to execute arbitrary JavaScript code as input for the workflow.]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Moooonet",
|
||||||
|
"title": "ComfyUI-ArteMoon [REMOVED]",
|
||||||
|
"reference": "https://github.com/Moooonet/ComfyUI-ArteMoon",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Moooonet/ComfyUI-ArteMoon"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This plugin works with [a/IF_AI_Tools](https://github.com/if-ai/ComfyUI-IF_AI_tools) to build a workflow in ComfyUI that uses AI to assist in generating prompts."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "ryanontheinside",
|
||||||
|
"title": "ComfyUI-MediaPipe-Vision [REMOVED]",
|
||||||
|
"reference": "https://github.com/ryanontheinside/ComfyUI-MediaPipe-Vision",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/ryanontheinside/ComfyUI-MediaPipe-Vision"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A centralized wrapper of all MediaPipe vision tasks for ComfyUI."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-textarea-command [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-textarea-command",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-textarea-command"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Add command and comment in textarea. (e.g. // Disabled line)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-parse-image [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-parse-image",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-parse-image"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Extract metadata from image."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-put-image [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-put-image",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-put-image"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Load image from directory."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "fredconex",
|
||||||
|
"title": "TripoSG Nodes for ComfyUI [REMOVED]",
|
||||||
|
"reference": "https://github.com/fredconex/ComfyUI-TripoSG",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/fredconex/ComfyUI-TripoSG"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Created by Alfredo Fernandes inspired by Hunyuan3D nodes by Kijai. This extension adds TripoSG 3D mesh generation capabilities to ComfyUI, allowing you to generate 3D meshes from a single image using the TripoSG model."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "fredconex",
|
||||||
|
"title": "ComfyUI-PaintTurbo [REMOVED]",
|
||||||
|
"reference": "https://github.com/fredconex/ComfyUI-PaintTurbo",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/fredconex/ComfyUI-PaintTurbo"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Hunyuan3D Texture Mesh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "zhuanqianfish",
|
||||||
|
"title": "TaesdDecoder [REMOVED]",
|
||||||
|
"reference": "https://github.com/zhuanqianfish/TaesdDecoder",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/zhuanqianfish/TaesdDecoder"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "use TAESD decoded image.you need donwload taesd_decoder.pth and taesdxl_decoder.pth to vae_approx folder first.\n It will result in a slight loss of image quality and a significant decrease in peak video memory during decoding."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "myAiLemon",
|
||||||
|
"title": "MagicAutomaticPicture [REMOVED]",
|
||||||
|
"reference": "https://github.com/myAiLemon/MagicAutomaticPicture",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/myAiLemon/MagicAutomaticPicture"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A comfyui node package that can generate pictures and automatically save positive prompts and eliminate unwanted prompts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "thisiseddy-ab",
|
||||||
|
"title": "ComfyUI-Edins-Ultimate-Pack [REMOVED]",
|
||||||
|
"reference": "https://github.com/thisiseddy-ab/ComfyUI-Edins-Ultimate-Pack",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/thisiseddy-ab/ComfyUI-Edins-Ultimate-Pack"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Well i needet a Tiled Ksampler that still works for Comfy UI there were none so i made one, in this Package i will put all Nodes i will develop for Comfy Ui still in beta alot will change.."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Davros666",
|
||||||
|
"title": "safetriggers [REMOVED]",
|
||||||
|
"reference": "https://github.com/Davros666/safetriggers",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Davros666/safetriggers"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "ComfyUI Nodes for READING TRIGGERS, TRIGGER-WORDS, TRIGGER-PHRASES FROM LoRAs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "cubiq",
|
||||||
|
"title": "Simple Math [REMOVED]",
|
||||||
|
"id": "simplemath",
|
||||||
|
"reference": "https://github.com/cubiq/ComfyUI_SimpleMath",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/cubiq/ComfyUI_SimpleMath"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "custom node for ComfyUI to perform simple math operations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "lucafoscili",
|
||||||
|
"title": "LF Nodes [DEPRECATED]",
|
||||||
|
"reference": "https://github.com/lucafoscili/comfyui-lf",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/lucafoscili/comfyui-lf"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Custom nodes with a touch of extra UX, including: history for primitives, JSON manipulation, logic switches with visual feedback, LLM chat... and more!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "AI2lab",
|
||||||
|
"title": "comfyUI-tool-2lab [REMOVED]",
|
||||||
|
"id": "tool-2lab",
|
||||||
|
"reference": "https://github.com/AI2lab/comfyUI-tool-2lab",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/AI2lab/comfyUI-tool-2lab"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "tool set for developing workflow and publish to web api server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "AI2lab",
|
||||||
|
"title": "comfyUI-DeepSeek-2lab [REMOVED]",
|
||||||
|
"id": "deepseek",
|
||||||
|
"reference": "https://github.com/AI2lab/comfyUI-DeepSeek-2lab",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/AI2lab/comfyUI-DeepSeek-2lab"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Unofficial implementation of DeepSeek for ComfyUI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "AI2lab",
|
||||||
|
"title": "comfyUI-kling-api-2lab [REMOVED]",
|
||||||
|
"reference": "https://github.com/AI2lab/comfyUI-kling-api-2lab",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/AI2lab/comfyUI-kling-api-2lab"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Unofficial implementation of KLing for ComfyUI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "ZhiHui6",
|
||||||
|
"title": "comfyui_zhihui_nodes [REMOVED]",
|
||||||
|
"reference": "https://github.com/ZhiHui6/comfyui_zhihui_nodes",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/ZhiHui6/comfyui_zhihui_nodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Prompt Preset, Video Batch Loader, Video Combiner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "ImagineerNL",
|
||||||
|
"title": "comfyui_potrace_svg [REMOVED]",
|
||||||
|
"reference": "https://github.com/ImagineerNL/comfyui_potrace_svg",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/ImagineerNL/comfyui_potrace_svg"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This project converts raster images into SVG format using the Potrace library."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "kayselmecnun",
|
||||||
|
"title": "ComfyUI-Qwen-25-VL [REMOVED]",
|
||||||
|
"reference": "https://github.com/kayselmecnun/ComfyUI-Qwen-25-VL",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/kayselmecnun/ComfyUI-Qwen-25-VL"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A custom Comfy UI node for using Qwen2.5-VL-3B/7B-Instruct models"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "IfnotFr",
|
||||||
|
"title": "⚡ ComfyUI Connect [REMOVED]",
|
||||||
|
"reference": "https://github.com/IfnotFr/ComfyUI-Connect",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/IfnotFr/ComfyUI-Connect"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Transform your ComfyUI into a powerful API, exposing all your saved workflows as ready-to-use HTTP endpoints."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "ginlov",
|
||||||
|
"title": "segment_to_mask_comfyui [REMOVED]",
|
||||||
|
"reference": "https://github.com/ginlov/segment_to_mask_comfyui",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/ginlov/segment_to_mask_comfyui"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Nodes:SegToMask"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "TGu-97",
|
||||||
|
"title": "TGu Utilities [REMOVED]",
|
||||||
|
"id": "tgu",
|
||||||
|
"reference": "https://github.com/TGu-97/ComfyUI-TGu-utils",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/TGu-97/ComfyUI-TGu-utils"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Nodes: MPN Switch, MPN Reroute, PN Switch. This is a set of custom nodes for ComfyUI. Mainly focus on control switches."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "IfnotFr",
|
||||||
|
"title": "ComfyUI-Connect [REMOVED]",
|
||||||
|
"reference": "https://github.com/IfnotFr/ComfyUI-Connect",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/IfnotFr/ComfyUI-Connect"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Transform your ComfyUI into a powerful API, exposing all your saved workflows as ready-to-use HTTP endpoints."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "KurtHokke",
|
||||||
|
"title": "ComfyUI_KurtHokke-Nodes [REMOVED]",
|
||||||
|
"reference": "https://github.com/KurtHokke/ComfyUI_KurtHokke-Nodes",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/KurtHokke/ComfyUI_KurtHokke-Nodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "ComfyUI_KurtHokke-Nodes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "SpatialDeploy",
|
||||||
|
"title": "ComfyUI-Voxels [REMOVED]",
|
||||||
|
"reference": "https://github.com/SpatialDeploy/ComfyUI-Voxels",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/SpatialDeploy/ComfyUI-Voxels"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Tools for creating voxel based videos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-group-selection [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-group-selection",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-group-selection"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Create a new group of nodes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "connect-from-afar [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-connect-from-afar",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-connect-from-afar"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Connect a new link from out of screen."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-local-db [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-local-db",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-local-db"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Store text to Key-Values pair json."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-model-db [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-model-db",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-model-db"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Store settings by model."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-target-search [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-target-search",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-target-search"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Move canvas to target on dragging connection."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "chrisgoringe",
|
||||||
|
"title": "Image chooser [DEPRECATED]",
|
||||||
|
"id": "image-chooser",
|
||||||
|
"reference": "https://github.com/chrisgoringe/cg-image-picker",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/chrisgoringe/cg-image-picker"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A custom node that pauses the flow while you choose which image (or latent) to pass on to the rest of the workflow."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "weilin9999",
|
||||||
|
"title": "WeiLin-ComfyUI-prompt-all-in-one [DEPRECATED]",
|
||||||
|
"id": "prompt-all-in-one",
|
||||||
|
"reference": "https://github.com/weilin9999/WeiLin-ComfyUI-prompt-all-in-one",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/weilin9999/WeiLin-ComfyUI-prompt-all-in-one"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Write prompt words like WebUI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "svetozarov",
|
||||||
|
"title": "AS_GeminiCaptioning Node [REMOVED]",
|
||||||
|
"reference": "https://github.com/svetozarov/AS_GeminiCaptioning",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/svetozarov/AS_GeminiCaptioning"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A ComfyUI node that combines an image with simple text parameters to create a prompt, sends it to the Google Gemini API via the google-generativeai SDK, and returns the generated text response along with the original prompt and an execution log"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-load-image-in-seq [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-load-image-in-seq",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-load-image-in-seq"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This node is load png image sequentially with metadata. Only supported for PNG format that has been created by ComfyUI.[w/renamed from comfyui-load-image-39. You need to remove previous one and reinstall to this.]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-model-metadata [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-model-metadata",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-model-metadata"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Print model metadata on note node"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "shinich39",
|
||||||
|
"title": "comfyui-view-recommendations [REMOVED]",
|
||||||
|
"reference": "https://github.com/shinich39/comfyui-view-recommendations",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/shinich39/comfyui-view-recommendations"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Load model generation data from civitai."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "jonstreeter",
|
||||||
|
"title": "Comfyui-PySceneDetect [REMOVED]",
|
||||||
|
"reference": "https://github.com/jonstreeter/Comfyui-PySceneDetect",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/jonstreeter/Comfyui-PySceneDetect"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: PySceneDetect Video Processor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "muxueChen",
|
||||||
|
"title": "ComfyUI-NTQwen25-VL [REMOVED]",
|
||||||
|
"reference": "https://github.com/muxueChen/ComfyUI-NTQwen25-VL",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/muxueChen/ComfyUI-NTQwen25-VL"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Qwen25-VL is a plugin for ComfyU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Makki_Shizu",
|
||||||
|
"title": "ComfyUI-SaveAnimatedGIF [DEPRECATED]",
|
||||||
|
"id": "SaveAnimatedGIF",
|
||||||
|
"reference": "https://github.com/MakkiShizu/ComfyUI-SaveAnimatedGIF",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/MakkiShizu/ComfyUI-SaveAnimatedGIF"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Save animated GIF format nodes in ComfyUI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "l1yongch1",
|
||||||
|
"title": "ComfyUI_PhiCaption [REMOVED]",
|
||||||
|
"reference": "https://github.com/l1yongch1/ComfyUI_PhiCaption",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/l1yongch1/ComfyUI_PhiCaption"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "In addition to achieving conventional single-image, single-round reverse engineering, it can also achieve single-image multi-round and multi-image single-round reverse engineering. Moreover, the Phi model has a better understanding of prompts."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "nova-florealis",
|
||||||
|
"title": "comfyui-alien [REMOVED]",
|
||||||
|
"reference": "https://github.com/nova-florealis/comfyui-alien",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/nova-florealis/comfyui-alien"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Text to Text (LLM), Text Output, Convert to Markdown, List Display (Debug)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "PluMaZero",
|
||||||
|
"title": "ComfyUI-SpaceFlower [REMOVED]",
|
||||||
|
"reference": "https://github.com/PluMaZero/ComfyUI-SpaceFlower",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/PluMaZero/ComfyUI-SpaceFlower"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Nodes: SpaceFlower_Prompt, SpaceFlower_HangulPrompt, ..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "vahidzxc",
|
||||||
|
"title": "ComfyUI-My-Handy-Nodes [REMOVED]",
|
||||||
|
"reference": "https://github.com/vahidzxc/ComfyUI-My-Handy-Nodes",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/vahidzxc/ComfyUI-My-Handy-Nodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES:VahCropImage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Samulebotin",
|
||||||
|
"title": "ComfyUI-FreeVC_wrapper [REMOVED]",
|
||||||
|
"reference": "https://github.com/Samulebotin/ComfyUI-FreeVC_wrapper",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Samulebotin/ComfyUI-FreeVC_wrapper"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A voice conversion extension node for ComfyUI based on FreeVC, enabling high-quality voice conversion capabilities within the ComfyUI framework."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "GoingAI1998",
|
||||||
|
"title": "ComfyUI Web Canvas Node [REMOVED]",
|
||||||
|
"reference": "https://github.com/GoingAI1998/Comfyui_imgcanvas",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/GoingAI1998/Comfyui_imgcanvas"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "ComfyUI_imgcanvas At present, I have not used the useful comfyui custom node about layer mixing, and I have written a comfyui runtime automatic pop-up window for layer editing node"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "807502278",
|
||||||
|
"title": "ComfyUI_TensorRT_Merge [REMOVED]",
|
||||||
|
"reference": "https://github.com/807502278/ComfyUI_TensorRT_Merge",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/807502278/ComfyUI_TensorRT_Merge"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Non diffusion models supported by TensorRT, merged Comfyui plugin, added onnx automatic download and trt model conversion nodes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "logtd",
|
||||||
|
"title": "ComfyUI-LTXTricks [DEPRECATED]",
|
||||||
|
"reference": "https://github.com/logtd/ComfyUI-LTXTricks",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/logtd/ComfyUI-LTXTricks"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A set of nodes that provide additional controls for the LTX Video model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "JichaoLiang",
|
||||||
|
"title": "Immortal_comfyUI [REMOVED]",
|
||||||
|
"reference": "https://github.com/JichaoLiang/Immortal_comfyUI",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/JichaoLiang/Immortal_comfyUI"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES:ImNewNode, ImAppendNode, MergeNode, SetProperties, SaveToDirectory, batchNodes, redirectToNode, SetEvent, ..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Rvage0815",
|
||||||
|
"title": "ComfyUI-RvTools [REMOVED]",
|
||||||
|
"reference": "https://github.com/Rvage0815/ComfyUI-RvTools",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Rvage0815/ComfyUI-RvTools"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "this node contains a lot of small little helpers like switches, passers and selectors that i use a lot to build my workflows."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Rvage0815",
|
||||||
|
"title": "RvTComfyUI-RvTools_v2 [REMOVED]",
|
||||||
|
"reference": "https://github.com/Rvage0815/ComfyUI-RvTools_v2",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Rvage0815/ComfyUI-RvTools_v2"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "this node contains a lot of small little helpers like switches, passers and selectors that i use a lot to build my workflows."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "scottmudge",
|
||||||
|
"title": "ComfyUI_BiscuitNodes [REMOVED]",
|
||||||
|
"reference": "https://github.com/scottmudge/ComfyUI_BiscuitNodes",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/scottmudge/ComfyUI_BiscuitNodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Load Image From Path Using File Selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "thanhduong0213929",
|
||||||
|
"title": "ComfyUI-DeepUnlock [REMOVED]",
|
||||||
|
"reference": "https://github.com/thanhduong0213929/ComfyUI-DeepUnlock",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/thanhduong0213929/ComfyUI-DeepUnlock"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "DeepFuze is a state-of-the-art deep learning tool that seamlessly integrates with ComfyUI to revolutionize facial transformations, lipsyncing, video generation, voice cloning, face swapping, and lipsync translation. Leveraging advanced algorithms, DeepFuze enables users to combine audio and video with unparalleled realism, ensuring perfectly synchronized facial movements. This innovative solution is ideal for content creators, animators, developers, and anyone seeking to elevate their video editing projects with sophisticated AI-driven features."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "pathway8-sudo",
|
||||||
|
"title": "RMBG [REMOVED]",
|
||||||
|
"reference": "https://github.com/pathway8-sudo/RMBG",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/pathway8-sudo/RMBG"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This repository provides a custom node for ComfyUI, leveraging the BriaRMBG model to remove backgrounds from images and output a transparent PNG."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "iris-Neko",
|
||||||
|
"title": "ComfyUI_ascii_art [REMOVED]",
|
||||||
|
"reference": "https://github.com/iris-Neko/ComfyUI_ascii_art",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/iris-Neko/ComfyUI_ascii_art"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "ComfyUI node for [a/ASCII art controlnet](https://civitai.com/models/986392)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "apesplat",
|
||||||
|
"title": "ezXY scripts and nodes [NOT MAINTAINED]",
|
||||||
|
"id": "ezxy",
|
||||||
|
"reference": "https://github.com/GMapeSplat/ComfyUI_ezXY",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/GMapeSplat/ComfyUI_ezXY"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Extensions/Patches: Enables linking float and integer inputs and ouputs. Values are automatically cast to the correct type and clamped to the correct range. Works with both builtin and custom nodes.[w/NOTE: This repo patches ComfyUI's validate_inputs and map_node_over_list functions while running. May break depending on your version of ComfyUI. Can be deactivated in config.yaml.]Nodes: A collection of nodes for facilitating the generation of XY plots. Capable of plotting changes over most primitive values.[w/Does not work with current version of Comfyui]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "mie",
|
||||||
|
"title": "ComfyUI_JanusProCaption [REMOVED]",
|
||||||
|
"reference": "https://github.com/MieMieeeee/ComfyUI-JanusProCaption",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/MieMieeeee/ComfyUI-JanusProCaption"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Describe image or create caption files using Janus Pro Model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Njbx",
|
||||||
|
"title": "ComfyUI-blockswap [REMOVED]",
|
||||||
|
"reference": "https://github.com/Njbx/ComfyUI-blockswap",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Njbx/ComfyUI-blockswap"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Block Swap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "T8star1984",
|
||||||
|
"title": "comfyui-purgevram [REMOVED]",
|
||||||
|
"reference": "https://github.com/T8star1984/comfyui-purgevram",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/T8star1984/comfyui-purgevram"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES:PurgeVRAM.\nCan be added after any node to clean up vram and memory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "zmwv823",
|
||||||
|
"title": "ComfyUI-VideoDiffusion [REMOVED]",
|
||||||
|
"reference": "https://github.com/zmwv823/ComfyUI-VideoDiffusion",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/zmwv823/ComfyUI-VideoDiffusion"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "[a/LatentSync](https://github.com/bytedance/LatentSync) and [a/Sonic](https://github.com/jixiaozhong/Sonic). [w/Just for study purpose. It's not for directly use, u should know how to fix issues.]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "NyaamZ",
|
||||||
|
"title": "Get Booru Tag ExtendeD [REMOVED]",
|
||||||
|
"reference": "https://github.com/NyaamZ/ComfyUI-GetBooruTag-ED",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/NyaamZ/ComfyUI-GetBooruTag-ED"
|
||||||
|
],
|
||||||
|
"description": "Get tag from Booru site.",
|
||||||
|
"install_type": "git-clone"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "lingha",
|
||||||
|
"title": "comfyui_kj [REMOVED]",
|
||||||
|
"id": "comfyui_kj",
|
||||||
|
"reference": "https://github.com/XieChengYuan/comfyui_kj",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/XieChengYuan/comfyui_kj"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "comfyui_kj, A tool that can package workflows into projects and publish them to a WeChat Mini Program named Kaji, allowing charges to be collected from users."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "myAiLemon",
|
||||||
|
"title": "MagicGetPromptAutomatically [REMOVED]",
|
||||||
|
"reference": "https://github.com/myAiLemon/MagicGetPromptAutomatically",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/myAiLemon/MagicGetPromptAutomatically"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A plug-in that can automatically generate pictures and save txt files in comfyui"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "ryanontheinside",
|
||||||
|
"title": "ComfyUI_ScavengerHunt [REMOVED]",
|
||||||
|
"reference": "https://github.com/ryanontheinside/ComfyUI_ScavengerHunt",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/ryanontheinside/ComfyUI_ScavengerHunt"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Compare Image Similarity (ResNet), Compare Image Similarity (CLIP), Compare Image Types\nNOTE: The files in the repo are not organized."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "vpakarinen",
|
||||||
|
"title": "ComfyUI-GenerationTimer [REMOVED]",
|
||||||
|
"reference": "https://github.com/vpakarinen/ComfyUI-GenerationTimer",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/vpakarinen/ComfyUI-GenerationTimer"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "NODES: Generation Timer, Image Timer, Timer Display"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "RedRayz",
|
||||||
|
"title": "ComfyUI-Danbooru-To-WD [REMOVED]",
|
||||||
|
"id": "danbooru2wd",
|
||||||
|
"reference": "https://github.com/RedRayz/ComfyUI-Danbooru-To-WD",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/RedRayz/ComfyUI-Danbooru-To-WD"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Converts booru tags to a format suitable for Waifu Diffusion(or Danbooru based models)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "alexgenovese",
|
||||||
|
"title": "comfyui_CfgPlusPlus [REMOVED]",
|
||||||
|
"id": "cfgpp",
|
||||||
|
"reference": "https://gitea.com/NotEvilGirl/cfgpp",
|
||||||
|
"files": [
|
||||||
|
"https://gitea.com/NotEvilGirl/cfgpp"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "CFG++ implemented according to [a/https://cfgpp-diffusion.github.io](https://cfgpp-diffusion.github.io). Basically modified DDIM sampler that makes sampling work at low CFG values (0 ~ 2). Read the CFG++ paper for more details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "hosterosi",
|
||||||
|
"title": "ComfyUI OpenAI Node",
|
||||||
|
"reference": "https://github.com/hosterosi/ComfyUI_OpenAI [REMOVED]",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/hosterosi/ComfyUI_OpenAI"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This custom node for ComfyUI allows users to input multiline text and select a specific line by its number. The node processes the input and returns the selected line along with its index."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "Gourieff",
|
||||||
|
"title": "ReActor Node for ComfyUI [DISABLED]",
|
||||||
|
"id": "reactor",
|
||||||
|
"reference": "https://github.com/Gourieff/comfyui-reactor-node",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Gourieff/comfyui-reactor-node"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "The Fast and Simple 'roop-like' Face Swap Extension Node for ComfyUI, based on ReActor (ex Roop-GE) SD-WebUI Face Swap Extension"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "prismwastaken",
|
||||||
|
"title": "prism-tools [REMOVED]",
|
||||||
|
"reference": "https://github.com/prismwastaken/prism-comfyui-tools",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/prismwastaken/prism-comfyui-tools"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "prism-tools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "42lux",
|
||||||
|
"title": "ComfyUI-safety-checker [DEPRECATED]",
|
||||||
|
"reference": "https://github.com/42lux/ComfyUI-safety-checker",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/42lux/ComfyUI-safety-checker"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A NSFW/Safety Checker Node for ComfyUI."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "riverolls",
|
||||||
|
"title": "ComfyUI-FJDH",
|
||||||
|
"reference": "https://github.com/riverolls/ComfyUI-FJDH [REMOVED]",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/riverolls/ComfyUI-FJDH"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "bbox tools, image tools, mask generators, point tools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "jetchopper",
|
||||||
|
"title": "ComfyUI-GeneraNodes",
|
||||||
|
"id": "genera",
|
||||||
|
"reference": "https://github.com/evolox/ComfyUI-GeneraNodes [REMOVED]",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/evolox/ComfyUI-GeneraNodes"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "Genera custom nodes and extensions"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"author": "Pos13",
|
"author": "Pos13",
|
||||||
"title": "Cyclist [DEPRECATED]",
|
"title": "Cyclist [DEPRECATED]",
|
||||||
|
|||||||
@@ -1,3 +1,148 @@
|
|||||||
{
|
{
|
||||||
"models": []
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "Inswapper-fp16 (face swap) [REMOVED]",
|
||||||
|
"type": "insightface",
|
||||||
|
"base": "inswapper",
|
||||||
|
"save_path": "insightface",
|
||||||
|
"description": "Checkpoint of the insightface swapper model\n(used by ComfyUI-FaceSwap, comfyui-reactor-node, CharacterFaceSwap,\nComfyUI roop and comfy_mtb)",
|
||||||
|
"reference": "https://github.com/facefusion/facefusion-assets",
|
||||||
|
"filename": "inswapper_128_fp16.onnx",
|
||||||
|
"url": "https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128_fp16.onnx",
|
||||||
|
"size": "277.7MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Inswapper (face swap) [REMOVED]",
|
||||||
|
"type": "insightface",
|
||||||
|
"base": "inswapper",
|
||||||
|
"save_path": "insightface",
|
||||||
|
"description": "Checkpoint of the insightface swapper model\n(used by ComfyUI-FaceSwap, comfyui-reactor-node, CharacterFaceSwap,\nComfyUI roop and comfy_mtb)",
|
||||||
|
"reference": "https://github.com/facefusion/facefusion-assets",
|
||||||
|
"filename": "inswapper_128.onnx",
|
||||||
|
"url": "https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx",
|
||||||
|
"size": "555.3MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pfg-novel-n10.pt",
|
||||||
|
"type": "PFG",
|
||||||
|
"base": "SD1.5",
|
||||||
|
"save_path": "custom_nodes/pfg-ComfyUI/models",
|
||||||
|
"description": "Pressing 'install' directly downloads the model from the pfg-ComfyUI/models extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
|
||||||
|
"reference": "https://huggingface.co/furusu/PFG",
|
||||||
|
"filename": "pfg-novel-n10.pt",
|
||||||
|
"url": "https://huggingface.co/furusu/PFG/resolve/main/pfg-novel-n10.pt",
|
||||||
|
"size": "23.6MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pfg-wd14-n10.pt",
|
||||||
|
"type": "PFG",
|
||||||
|
"base": "SD1.5",
|
||||||
|
"save_path": "custom_nodes/pfg-ComfyUI/models",
|
||||||
|
"description": "Pressing 'install' directly downloads the model from the pfg-ComfyUI/models extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
|
||||||
|
"reference": "https://huggingface.co/furusu/PFG",
|
||||||
|
"filename": "pfg-wd14-n10.pt",
|
||||||
|
"url": "https://huggingface.co/furusu/PFG/resolve/main/pfg-wd14-n10.pt",
|
||||||
|
"size": "31.5MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pfg-wd15beta2-n10.pt",
|
||||||
|
"type": "PFG",
|
||||||
|
"base": "SD1.5",
|
||||||
|
"save_path": "custom_nodes/pfg-ComfyUI/models",
|
||||||
|
"description": "Pressing 'install' directly downloads the model from the pfg-ComfyUI/models extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
|
||||||
|
"reference": "https://huggingface.co/furusu/PFG",
|
||||||
|
"filename": "pfg-wd15beta2-n10.pt",
|
||||||
|
"url": "https://huggingface.co/furusu/PFG/resolve/main/pfg-wd15beta2-n10.pt",
|
||||||
|
"size": "31.5MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shape_predictor_68_face_landmarks.dat [Face Analysis]",
|
||||||
|
"type": "Shape Predictor",
|
||||||
|
"base": "DLIB",
|
||||||
|
"save_path": "custom_nodes/comfyui_faceanalysis/dlib",
|
||||||
|
"description": "To use the Face Analysis for ComfyUI custom node, installation of this model is needed.",
|
||||||
|
"reference": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/tree/main",
|
||||||
|
"filename": "shape_predictor_68_face_landmarks.dat",
|
||||||
|
"url": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/resolve/main/shape_predictor_68_face_landmarks.dat",
|
||||||
|
"size": "99.7MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dlib_face_recognition_resnet_model_v1.dat [Face Analysis]",
|
||||||
|
"type": "Face Recognition",
|
||||||
|
"base": "DLIB",
|
||||||
|
"save_path": "custom_nodes/comfyui_faceanalysis/dlib",
|
||||||
|
"description": "To use the Face Analysis for ComfyUI custom node, installation of this model is needed.",
|
||||||
|
"reference": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/tree/main",
|
||||||
|
"filename": "dlib_face_recognition_resnet_model_v1.dat",
|
||||||
|
"url": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/resolve/main/dlib_face_recognition_resnet_model_v1.dat",
|
||||||
|
"size": "22.5MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ID-Animator/animator.ckpt",
|
||||||
|
"type": "ID-Animator",
|
||||||
|
"base": "SD1.5",
|
||||||
|
"save_path": "custom_nodes/comfyui_id_animator/models",
|
||||||
|
"description": "ID-Animator checkpoint",
|
||||||
|
"reference": "https://huggingface.co/spaces/ID-Animator/ID-Animator",
|
||||||
|
"filename": "animator.ckpt",
|
||||||
|
"url": "https://huggingface.co/spaces/ID-Animator/ID-Animator/resolve/main/animator.ckpt",
|
||||||
|
"size": "247.3MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ID-Animator/mm_sd_v15_v2.ckpt",
|
||||||
|
"type": "ID-Animator",
|
||||||
|
"base": "SD1.5",
|
||||||
|
"save_path": "custom_nodes/comfyui_id_animator/models/animatediff_models",
|
||||||
|
"description": "AnimateDiff checkpoint for ID-Animator",
|
||||||
|
"reference": "https://huggingface.co/spaces/ID-Animator/ID-Animator",
|
||||||
|
"filename": "mm_sd_v15_v2.ckpt",
|
||||||
|
"url": "https://huggingface.co/spaces/ID-Animator/ID-Animator/resolve/main/mm_sd_v15_v2.ckpt",
|
||||||
|
"size": "1.82GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ID-Animator/image_encoder",
|
||||||
|
"type": "ID-Animator",
|
||||||
|
"base": "SD1.5",
|
||||||
|
"save_path": "custom_nodes/comfyui_id_animator/models/image_encoder",
|
||||||
|
"description": "CLIP Image encoder for ID-Animator",
|
||||||
|
"reference": "https://huggingface.co/spaces/ID-Animator/ID-Animator",
|
||||||
|
"filename": "model.safetensors",
|
||||||
|
"url": "https://huggingface.co/spaces/ID-Animator/ID-Animator/resolve/main/image_encoder/model.safetensors",
|
||||||
|
"size": "2.53GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Doubiiu/ToonCrafter model checkpoint",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "ToonCrafter",
|
||||||
|
"save_path": "custom_nodes/comfyui-tooncrafter/ToonCrafter/checkpoints/tooncrafter_512_interp_v1",
|
||||||
|
"description": "ToonCrafter checkpoint model for ComfyUI-ToonCrafter",
|
||||||
|
"reference": "https://huggingface.co/Doubiiu/ToonCrafter/tree/main",
|
||||||
|
"filename": "model.ckpt",
|
||||||
|
"url": "https://huggingface.co/Doubiiu/ToonCrafter/resolve/main/model.ckpt",
|
||||||
|
"size": "10.5GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "BAAI/SegGPT",
|
||||||
|
"type": "SegGPT",
|
||||||
|
"base": "SegGPT",
|
||||||
|
"save_path": "custom_nodes/comfyui-seggpt",
|
||||||
|
"description": "SegGPT",
|
||||||
|
"reference": "https://huggingface.co/BAAI/SegGPT",
|
||||||
|
"filename": "seggpt_vit_large.pth",
|
||||||
|
"url": "https://huggingface.co/BAAI/SegGPT/resolve/main/seggpt_vit_large.pth",
|
||||||
|
"size": "1.48GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kohya-ss/ControlNet-LLLite: SDXL Canny Anime",
|
||||||
|
"type": "controlnet",
|
||||||
|
"base": "SDXL",
|
||||||
|
"save_path": "custom_nodes/ControlNet-LLLite-ComfyUI/models",
|
||||||
|
"description": "An extremely compactly designed controlnet model (a.k.a. ControlNet-LLLite). Note: The model structure is highly experimental and may be subject to change in the future.",
|
||||||
|
"reference": "https://huggingface.co/kohya-ss/controlnet-lllite",
|
||||||
|
"filename": "controllllite_v01032064e_sdxl_canny_anime.safetensors",
|
||||||
|
"url": "https://huggingface.co/kohya-ss/controlnet-lllite/resolve/main/controllllite_v01032064e_sdxl_canny_anime.safetensors",
|
||||||
|
"size": "46.2MB"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,457 @@
|
|||||||
{
|
{
|
||||||
"models": [
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "Latent Bridge Matching for Image Relighting",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "LBM",
|
||||||
|
"save_path": "diffusion_models/LBM",
|
||||||
|
"description": "Latent Bridge Matching (LBM) Relighting model",
|
||||||
|
"reference": "https://huggingface.co/jasperai/LBM_relighting",
|
||||||
|
"filename": "LBM_relighting.safetensors",
|
||||||
|
"url": "https://huggingface.co/jasperai/LBM_relighting/resolve/main/model.safetensors",
|
||||||
|
"size": "5.02GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B Distilled v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "Distilled version of the LTX-Video 13B model, providing improved efficiency while maintaining high-resolution quality.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-distilled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled.safetensors",
|
||||||
|
"size": "28.6GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B Distilled FP8 v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "Quantized distilled version of the LTX-Video 13B model, optimized for even lower VRAM usage while maintaining quality.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-distilled-fp8.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-fp8.safetensors",
|
||||||
|
"size": "15.7GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B Distilled LoRA v0.9.7",
|
||||||
|
"type": "lora",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "loras",
|
||||||
|
"description": "A LoRA adapter that transforms the standard LTX-Video 13B model into a distilled version when loaded.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-distilled-lora128.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-lora128.safetensors",
|
||||||
|
"size": "1.33GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lllyasviel/FramePackI2V_HY",
|
||||||
|
"type": "FramePackI2V",
|
||||||
|
"base": "FramePackI2V",
|
||||||
|
"save_path": "diffusers/lllyasviel",
|
||||||
|
"description": "[SNAPSHOT] This is the f1k1_x_g9_f1k1f2k2f16k4_td FramePack for HY. [w/You cannot download this item on ComfyUI-Manager versions below V3.18]",
|
||||||
|
"reference": "https://huggingface.co/lllyasviel/FramePackI2V_HY",
|
||||||
|
"filename": "<huggingface>",
|
||||||
|
"url": "lllyasviel/FramePackI2V_HY",
|
||||||
|
"size": "25.75GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "LTX-Video Spatial Upscaler v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "Spatial upscaler model for LTX-Video. This model enhances the spatial resolution of generated videos.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-spatial-upscaler-0.9.7.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-spatial-upscaler-0.9.7.safetensors",
|
||||||
|
"size": "505MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video Temporal Upscaler v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "Temporal upscaler model for LTX-Video. This model enhances the temporal resolution and smoothness of generated videos.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-temporal-upscaler-0.9.7.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-temporal-upscaler-0.9.7.safetensors",
|
||||||
|
"size": "524MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "High-resolution quality LTX-Video 13B model.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-dev.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-dev.safetensors",
|
||||||
|
"size": "28.6GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 13B FP8 v0.9.7",
|
||||||
|
"type": "checkpoint",
|
||||||
|
"base": "LTX-Video",
|
||||||
|
"save_path": "checkpoints/LTXV",
|
||||||
|
"description": "Quantized version of the LTX-Video 13B model, optimized for lower VRAM usage while maintaining high quality.",
|
||||||
|
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||||
|
"filename": "ltxv-13b-0.9.7-dev-fp8.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-dev-fp8.safetensors",
|
||||||
|
"size": "15.7GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 480p 14B (bf16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 480p 14B (bf16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_480p_14B_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_480p_14B_bf16.safetensors",
|
||||||
|
"size": "32.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 480p 14B (fp16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 480p 14B (fp16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_480p_14B_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_480p_14B_fp16.safetensors",
|
||||||
|
"size": "32.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 480p 14B (fp8_e4m3fn)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 480p 14B (fp8_e4m3fn)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_480p_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_480p_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"size": "16.4GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 480p 14B (fp8_scaled)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 480p 14B (fp8_scaled)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_480p_14B_fp8_scaled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_480p_14B_fp8_scaled.safetensors",
|
||||||
|
"size": "16.4GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 720p 14B (bf16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 720p 14B (bf16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_720p_14B_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_720p_14B_bf16.safetensors",
|
||||||
|
"size": "32.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 720p 14B (fp16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 720p 14B (fp16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_720p_14B_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_720p_14B_fp16.safetensors",
|
||||||
|
"size": "32.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 720p 14B (fp8_e4m3fn)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 720p 14B (fp8_e4m3fn)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_720p_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_720p_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"size": "16.4GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 i2v 720p 14B (fp8_scaled)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for i2v 720p 14B (fp8_scaled)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_i2v_720p_14B_fp8_scaled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_720p_14B_fp8_scaled.safetensors",
|
||||||
|
"size": "16.4GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/clip_vision_h.safetensors",
|
||||||
|
"type": "clip_vision",
|
||||||
|
"base": "clip_vision_h",
|
||||||
|
"save_path": "clip_vision",
|
||||||
|
"description": "clip_vision_h model for Wan2.1",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "clip_vision_h.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/clip_vision/clip_vision_h.safetensors",
|
||||||
|
"size": "1.26GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 1.3B (bf16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 1.3B (bf16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_1.3B_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_1.3B_bf16.safetensors",
|
||||||
|
"size": "2.84GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 1.3B (fp16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 1.3B (fp16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_1.3B_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_1.3B_fp16.safetensors",
|
||||||
|
"size": "2.84GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 14B (bf16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 14B (bf16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_14B_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_14B_bf16.safetensors",
|
||||||
|
"size": "28.6GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 14B (fp16)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 14B (fp16)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_14B_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_14B_fp16.safetensors",
|
||||||
|
"size": "28.6GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 14B (fp8_e4m3fn)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 14B (fp8_e4m3fn)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_14B_fp8_e4m3fn.safetensors",
|
||||||
|
"size": "14.3GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 t2v 14B (fp8_scaled)",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "diffusion_models/Wan2.1",
|
||||||
|
"description": "Wan2.1 difussion model for t2v 14B (fp8_scaled)",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan2.1_t2v_14B_fp8_scaled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_14B_fp8_scaled.safetensors",
|
||||||
|
"size": "14.3GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/Wan2.1 VAE",
|
||||||
|
"type": "vae",
|
||||||
|
"base": "Wan2.1",
|
||||||
|
"save_path": "vae",
|
||||||
|
"description": "Wan2.1 VAE model",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "wan_2.1_vae.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/vae/wan_2.1_vae.safetensors",
|
||||||
|
"size": "254MB"
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/umt5_xxl_fp16.safetensors",
|
||||||
|
"type": "clip",
|
||||||
|
"base": "umt5_xxl",
|
||||||
|
"save_path": "text_encoders",
|
||||||
|
"description": "umt5_xxl_fp16 text encoder for Wan2.1",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "umt5_xxl_fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp16.safetensors",
|
||||||
|
"size": "11.4GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||||
|
"type": "clip",
|
||||||
|
"base": "umt5_xxl",
|
||||||
|
"save_path": "text_encoders",
|
||||||
|
"description": "umt5_xxl_fp8_e4m3fn_scaled text encoder for Wan2.1",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged",
|
||||||
|
"filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||||
|
"size": "6.74GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/hunyuan_video_image_to_video_720p_bf16.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Hunyuan Video",
|
||||||
|
"save_path": "diffusion_models/hunyuan_video",
|
||||||
|
"description": "Huyuan Video Image2Video diffusion model. repackaged version.",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged",
|
||||||
|
"filename": "hunyuan_video_image_to_video_720p_bf16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/diffusion_models/hunyuan_video_image_to_video_720p_bf16.safetensors",
|
||||||
|
"size": "25.6GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comfy-Org/llava_llama3_vision.safetensors",
|
||||||
|
"type": "clip_vision",
|
||||||
|
"base": "LLaVA-Llama-3",
|
||||||
|
"save_path": "text_encoders",
|
||||||
|
"description": "llava_llama3_vision clip vison model. This is required for using Hunyuan Video Image2Video.",
|
||||||
|
"reference": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged",
|
||||||
|
"filename": "llava_llama3_vision.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/clip_vision/llava_llama3_vision.safetensors",
|
||||||
|
"size": "649MB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "LTX-Video 2B v0.9.5 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.5.safetensors",
|
||||||
|
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.5.safetensors",
|
||||||
|
"size": "6.34GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kolors/vae/diffusion_pytorch_model.fp16.safetensors",
|
||||||
|
"type": "VAE",
|
||||||
|
"base": "Kolors",
|
||||||
|
"save_path": "vae/kolors",
|
||||||
|
"description": "Kolors VAE",
|
||||||
|
"reference": "https://huggingface.co/Kwai-Kolors/Kolors",
|
||||||
|
"filename": "diffusion_pytorch_model.fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Kwai-Kolors/Kolors/resolve/main/vae/diffusion_pytorch_model.fp16.safetensors",
|
||||||
|
"size": "167MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kolors/vae/diffusion_pytorch_model.safetensors",
|
||||||
|
"type": "VAE",
|
||||||
|
"base": "Kolors",
|
||||||
|
"save_path": "vae/kolors",
|
||||||
|
"description": "Kolors VAE",
|
||||||
|
"reference": "https://huggingface.co/Kwai-Kolors/Kolors",
|
||||||
|
"filename": "diffusion_pytorch_model.safetensors",
|
||||||
|
"url": "https://huggingface.co/Kwai-Kolors/Kolors/resolve/main/vae/diffusion_pytorch_model.safetensors",
|
||||||
|
"size": "335MB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "deepseek-ai/Janus-Pro-1B",
|
||||||
|
"type": "Janus-Pro",
|
||||||
|
"base": "Janus-Pro",
|
||||||
|
"save_path": "Janus-Pro",
|
||||||
|
"description": "[SNAPSHOT] Janus-Pro-1B model.[w/You cannot download this item on ComfyUI-Manager versions below V3.18]",
|
||||||
|
"reference": "https://huggingface.co/deepseek-ai/Janus-Pro-1B",
|
||||||
|
"filename": "<huggingface>",
|
||||||
|
"url": "deepseek-ai/Janus-Pro-1B",
|
||||||
|
"size": "7.8GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deepseek-ai/Janus-Pro-7B",
|
||||||
|
"type": "Janus-Pro",
|
||||||
|
"base": "Janus-Pro",
|
||||||
|
"save_path": "Janus-Pro",
|
||||||
|
"description": "[SNAPSHOT] Janus-Pro-7B model.[w/You cannot download this item on ComfyUI-Manager versions below V3.18]",
|
||||||
|
"reference": "https://huggingface.co/deepseek-ai/Janus-Pro-7B",
|
||||||
|
"filename": "<huggingface>",
|
||||||
|
"url": "deepseek-ai/Janus-Pro-7B",
|
||||||
|
"size": "14.85GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Leoxing/pia.ckpt",
|
||||||
|
"type": "animatediff-pia",
|
||||||
|
"base": "SD1.x",
|
||||||
|
"save_path": "animatediff_models",
|
||||||
|
"description": "AnimateDiff-PIA Model",
|
||||||
|
"reference": "https://huggingface.co/Leoxing/PIA/tree/main",
|
||||||
|
"filename": "pia.ckpt",
|
||||||
|
"url": "https://huggingface.co/Leoxing/PIA/resolve/main/pia.ckpt",
|
||||||
|
"size": "1.67GB"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "comfyanonymous/cosmos_cv8x8x8_1.0.safetensors",
|
||||||
|
"type": "VAE",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "default",
|
||||||
|
"description": "VAE model for Cosmos 1.0",
|
||||||
|
"reference": "https://huggingface.co/comfyanonymous/cosmos_1.0_text_encoder_and_VAE_ComfyUI/tree/main",
|
||||||
|
"filename": "cosmos_cv8x8x8_1.0.safetensors",
|
||||||
|
"url": "https://huggingface.co/comfyanonymous/cosmos_1.0_text_encoder_and_VAE_ComfyUI/resolve/main/vae/cosmos_cv8x8x8_1.0.safetensors",
|
||||||
|
"size": "211MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mcmonkey/Cosmos-1_0-Diffusion-7B-Text2World.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "diffusion_models/cosmos-1.0",
|
||||||
|
"description": "Cosmos 1.0 Text2World Diffusion Model (7B)",
|
||||||
|
"reference": "https://huggingface.co/mcmonkey/cosmos-1.0",
|
||||||
|
"filename": "Cosmos-1_0-Diffusion-7B-Text2World.safetensors",
|
||||||
|
"url": "https://huggingface.co/mcmonkey/cosmos-1.0/resolve/main/Cosmos-1_0-Diffusion-7B-Text2World.safetensors",
|
||||||
|
"size": "14.5GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mcmonkey/Cosmos-1_0-Diffusion-7B-Video2World.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "diffusion_models/cosmos-1.0",
|
||||||
|
"description": "Cosmos 1.0 Video2World Diffusion Model (7B)",
|
||||||
|
"reference": "https://huggingface.co/mcmonkey/cosmos-1.0",
|
||||||
|
"filename": "Cosmos-1_0-Diffusion-7B-Video2World.safetensors",
|
||||||
|
"url": "https://huggingface.co/mcmonkey/cosmos-1.0/resolve/main/Cosmos-1_0-Diffusion-7B-Video2World.safetensors",
|
||||||
|
"size": "14.5GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mcmonkey/Cosmos-1_0-Diffusion-14B-Text2World.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "diffusion_models/cosmos-1.0",
|
||||||
|
"description": "Cosmos 1.0 Text2World Diffusion Model (14B)",
|
||||||
|
"reference": "https://huggingface.co/mcmonkey/cosmos-1.0",
|
||||||
|
"filename": "Cosmos-1_0-Diffusion-14B-Text2World.safetensors",
|
||||||
|
"url": "https://huggingface.co/mcmonkey/cosmos-1.0/resolve/main/Cosmos-1_0-Diffusion-14B-Text2World.safetensors",
|
||||||
|
"size": "28.5GB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mcmonkey/Cosmos-1_0-Diffusion-14B-Video2World.safetensors",
|
||||||
|
"type": "diffusion_model",
|
||||||
|
"base": "Cosmos-1.0",
|
||||||
|
"save_path": "diffusion_models/cosmos-1.0",
|
||||||
|
"description": "Cosmos 1.0 Video2World Diffusion Model (14B)",
|
||||||
|
"reference": "https://huggingface.co/mcmonkey/cosmos-1.0",
|
||||||
|
"filename": "Cosmos-1_0-Diffusion-14B-Video2World.safetensors",
|
||||||
|
"url": "https://huggingface.co/mcmonkey/cosmos-1.0/resolve/main/Cosmos-1_0-Diffusion-14B-Video2World.safetensors",
|
||||||
|
"size": "28.5GB"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Comfy-Org/llava_llama3_fp8_scaled.safetensors",
|
"name": "Comfy-Org/llava_llama3_fp8_scaled.safetensors",
|
||||||
"type": "clip",
|
"type": "clip",
|
||||||
@@ -239,495 +691,6 @@
|
|||||||
"filename": "sigclip_vision_patch14_384.safetensors",
|
"filename": "sigclip_vision_patch14_384.safetensors",
|
||||||
"url": "https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors",
|
"url": "https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors",
|
||||||
"size": "857MB"
|
"size": "857MB"
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "comfyanonymous/flux_text_encoders - t5xxl (fp16)",
|
|
||||||
"type": "clip",
|
|
||||||
"base": "t5",
|
|
||||||
"save_path": "text_encoders/t5",
|
|
||||||
"description": "Text Encoders for FLUX (fp16)",
|
|
||||||
"reference": "https://huggingface.co/comfyanonymous/flux_text_encoders",
|
|
||||||
"filename": "t5xxl_fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors",
|
|
||||||
"size": "9.79GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "comfyanonymous/flux_text_encoders - t5xxl (fp8_e4m3fn)",
|
|
||||||
"type": "clip",
|
|
||||||
"base": "t5",
|
|
||||||
"save_path": "text_encoders/t5",
|
|
||||||
"description": "Text Encoders for FLUX (fp8_e4m3fn)",
|
|
||||||
"reference": "https://huggingface.co/comfyanonymous/flux_text_encoders",
|
|
||||||
"filename": "t5xxl_fp8_e4m3fn.safetensors",
|
|
||||||
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn.safetensors",
|
|
||||||
"size": "4.89GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "comfyanonymous/flux_text_encoders - t5xxl (fp8_e4m3fn_scaled)",
|
|
||||||
"type": "clip",
|
|
||||||
"base": "t5",
|
|
||||||
"save_path": "text_encoders/t5",
|
|
||||||
"description": "Text Encoders for FLUX (fp16)",
|
|
||||||
"reference": "https://huggingface.co/comfyanonymous/flux_text_encoders",
|
|
||||||
"filename": "t5xxl_fp8_e4m3fn_scaled.safetensors",
|
|
||||||
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn_scaled.safetensors",
|
|
||||||
"size": "5.16GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "FLUX.1 [Dev] Diffusion model (scaled fp8)",
|
|
||||||
"type": "diffusion_model",
|
|
||||||
"base": "FLUX.1",
|
|
||||||
"save_path": "diffusion_models/FLUX1",
|
|
||||||
"description": "FLUX.1 [Dev] Diffusion model (scaled fp8)[w/Due to the large size of the model, it is recommended to download it through a browser if possible.]",
|
|
||||||
"reference": "https://huggingface.co/comfyanonymous/flux_dev_scaled_fp8_test",
|
|
||||||
"filename": "flux_dev_fp8_scaled_diffusion_model.safetensors",
|
|
||||||
"url": "https://huggingface.co/comfyanonymous/flux_dev_scaled_fp8_test/resolve/main/flux_dev_fp8_scaled_diffusion_model.safetensors",
|
|
||||||
"size": "11.9GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "kijai/MoGe_ViT_L_fp16.safetensors",
|
|
||||||
"type": "MoGe",
|
|
||||||
"base": "MoGe",
|
|
||||||
"save_path": "MoGe",
|
|
||||||
"description": "Safetensors versions of [a/https://github.com/microsoft/MoGe](https://github.com/microsoft/MoGe)",
|
|
||||||
"reference": "https://huggingface.co/Kijai/MoGe_safetensors",
|
|
||||||
"filename": "MoGe_ViT_L_fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/MoGe_safetensors/resolve/main/MoGe_ViT_L_fp16.safetensors",
|
|
||||||
"size": "628MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kijai/MoGe_ViT_L_fp16.safetensors",
|
|
||||||
"type": "MoGe",
|
|
||||||
"base": "MoGe",
|
|
||||||
"save_path": "MoGe",
|
|
||||||
"description": "Safetensors versions of [a/https://github.com/microsoft/MoGe](https://github.com/microsoft/MoGe)",
|
|
||||||
"reference": "https://huggingface.co/Kijai/MoGe_safetensors",
|
|
||||||
"filename": "MoGe_ViT_L_fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/MoGe_safetensors/resolve/main/MoGe_ViT_L_fp16.safetensors",
|
|
||||||
"size": "1.26GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "pulid_flux_v0.9.1.safetensors",
|
|
||||||
"type": "PuLID",
|
|
||||||
"base": "FLUX",
|
|
||||||
"save_path": "pulid",
|
|
||||||
"description": "This is required for PuLID (FLUX)",
|
|
||||||
"reference": "https://huggingface.co/guozinan/PuLID",
|
|
||||||
"filename": "pulid_flux_v0.9.1.safetensors",
|
|
||||||
"url": "https://huggingface.co/guozinan/PuLID/resolve/main/pulid_flux_v0.9.1.safetensors",
|
|
||||||
"size": "1.14GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pulid_v1.1.safetensors",
|
|
||||||
"type": "PuLID",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "pulid",
|
|
||||||
"description": "This is required for PuLID (SDXL)",
|
|
||||||
"reference": "https://huggingface.co/guozinan/PuLID",
|
|
||||||
"filename": "pulid_v1.1.safetensors",
|
|
||||||
"url": "https://huggingface.co/guozinan/PuLID/resolve/main/pulid_v1.1.safetensors",
|
|
||||||
"size": "984MB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Kolors-IP-Adapter-Plus.bin (Kwai-Kolors/Kolors-IP-Adapter-Plus)",
|
|
||||||
"type": "IP-Adapter",
|
|
||||||
"base": "Kolors",
|
|
||||||
"save_path": "ipadapter",
|
|
||||||
"description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
|
|
||||||
"reference": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus",
|
|
||||||
"filename": "Kolors-IP-Adapter-Plus.bin",
|
|
||||||
"url": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus/resolve/main/ip_adapter_plus_general.bin",
|
|
||||||
"size": "1.01GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Kolors-IP-Adapter-FaceID-Plus.bin (Kwai-Kolors/Kolors-IP-Adapter-Plus)",
|
|
||||||
"type": "IP-Adapter",
|
|
||||||
"base": "Kolors",
|
|
||||||
"save_path": "ipadapter",
|
|
||||||
"description": "You can use this model in the [a/ComfyUI IPAdapter plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus) extension.",
|
|
||||||
"reference": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-FaceID-Plus",
|
|
||||||
"filename": "Kolors-IP-Adapter-FaceID-Plus.bin",
|
|
||||||
"url": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-FaceID-Plus/resolve/main/ipa-faceid-plus.bin",
|
|
||||||
"size": "2.39GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "CLIPVision model (Kwai-Kolors/Kolors-IP-Adapter-Plus/clip-vit-large)",
|
|
||||||
"type": "clip_vision",
|
|
||||||
"base": "ViT-L",
|
|
||||||
"save_path": "clip_vision",
|
|
||||||
"description": "CLIPVision model (This is required in cubiq/ComfyUI_IPAdapter_plus)",
|
|
||||||
"reference": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus",
|
|
||||||
"filename": "clip-vit-large-patch14-336.bin",
|
|
||||||
"url": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus/resolve/main/image_encoder/pytorch_model.bin",
|
|
||||||
"size": "1.71GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "kijai/lotus depth d model v1.1 (fp16)",
|
|
||||||
"type": "diffusion_model",
|
|
||||||
"base": "lotus",
|
|
||||||
"save_path": "diffusion_models",
|
|
||||||
"description": "lotus depth d model v1.1 (fp16). This model can be used in ComfyUI-Lotus custom nodes.",
|
|
||||||
"reference": "https://huggingface.co/Kijai/lotus-comfyui",
|
|
||||||
"filename": "lotus-depth-d-v-1-1-fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/lotus-comfyui/resolve/main/lotus-depth-d-v-1-1-fp16.safetensors",
|
|
||||||
"size": "1.74GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kijai/lotus depth g model v1.0 (fp16)",
|
|
||||||
"type": "diffusion_model",
|
|
||||||
"base": "lotus",
|
|
||||||
"save_path": "diffusion_models",
|
|
||||||
"description": "lotus depth g model v1.0 (fp16). This model can be used in ComfyUI-Lotus custom nodes.",
|
|
||||||
"reference": "https://huggingface.co/Kijai/lotus-comfyui",
|
|
||||||
"filename": "lotus-depth-g-v1-0-fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/lotus-comfyui/resolve/main/lotus-depth-g-v1-0-fp16.safetensors",
|
|
||||||
"size": "1.74GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kijai/lotus depth g model v1.0",
|
|
||||||
"type": "diffusion_model",
|
|
||||||
"base": "lotus",
|
|
||||||
"save_path": "diffusion_models",
|
|
||||||
"description": "lotus depth g model v1.0. This model can be used in ComfyUI-Lotus custom nodes.",
|
|
||||||
"reference": "https://huggingface.co/Kijai/lotus-comfyui",
|
|
||||||
"filename": "lotus-depth-g-v1-0.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/lotus-comfyui/resolve/main/lotus-depth-g-v1-0.safetensors",
|
|
||||||
"size": "3.47GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kijai/lotus normal d model v1.0 (fp16)",
|
|
||||||
"type": "diffusion_model",
|
|
||||||
"base": "lotus",
|
|
||||||
"save_path": "diffusion_models",
|
|
||||||
"description": "lotus normal d model v1.0 (fp16). This model can be used in ComfyUI-Lotus custom nodes.",
|
|
||||||
"reference": "https://huggingface.co/Kijai/lotus-comfyui",
|
|
||||||
"filename": "lotus-normal-d-v1-0-fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/lotus-comfyui/resolve/main/lotus-normal-d-v1-0-fp16.safetensors",
|
|
||||||
"size": "1.74GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kijai/lotus normal d model v1.0",
|
|
||||||
"type": "diffusion_model",
|
|
||||||
"base": "lotus",
|
|
||||||
"save_path": "diffusion_models",
|
|
||||||
"description": "lotus normal d model v1.0. This model can be used in ComfyUI-Lotus custom nodes.",
|
|
||||||
"reference": "https://huggingface.co/Kijai/lotus-comfyui",
|
|
||||||
"filename": "lotus-normal-d-v1-0.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/lotus-comfyui/resolve/main/lotus-normal-d-v1-0.safetensors",
|
|
||||||
"size": "3.47GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kijai/lotus normal g model v1.0 (fp16)",
|
|
||||||
"type": "diffusion_model",
|
|
||||||
"base": "lotus",
|
|
||||||
"save_path": "diffusion_models",
|
|
||||||
"description": "lotus normal g model v1.0 (fp16). This model can be used in ComfyUI-Lotus custom nodes.",
|
|
||||||
"reference": "https://huggingface.co/Kijai/lotus-comfyui",
|
|
||||||
"filename": "lotus-normal-g-v1-0-fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/lotus-comfyui/resolve/main/lotus-normal-g-v1-0-fp16.safetensors",
|
|
||||||
"size": "1.74GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "kijai/lotus normal g model v1.0",
|
|
||||||
"type": "diffusion_model",
|
|
||||||
"base": "lotus",
|
|
||||||
"save_path": "diffusion_models",
|
|
||||||
"description": "lotus normal g model v1.0. This model can be used in ComfyUI-Lotus custom nodes.",
|
|
||||||
"reference": "https://huggingface.co/Kijai/lotus-comfyui",
|
|
||||||
"filename": "lotus-normal-g-v1-0.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/lotus-comfyui/resolve/main/lotus-normal-g-v1-0.safetensors",
|
|
||||||
"size": "3.47GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Depth Pro model",
|
|
||||||
"type": "depth-pro",
|
|
||||||
"base": "depth-pro",
|
|
||||||
"save_path": "depth/ml-depth-pro",
|
|
||||||
"description": "Depth pro model for [a/ComfyUI-Depth-Pro](https://github.com/spacepxl/ComfyUI-Depth-Pro)",
|
|
||||||
"reference": "https://huggingface.co/spacepxl/ml-depth-pro",
|
|
||||||
"filename": "depth_pro.fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/spacepxl/ml-depth-pro/resolve/main/depth_pro.fp16.safetensors",
|
|
||||||
"size": "1.9GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "jasperai/FLUX.1-dev-Controlnet-Upscaler",
|
|
||||||
"type": "controlnet",
|
|
||||||
"base": "FLUX.1",
|
|
||||||
"save_path": "controlnet/FLUX.1/jasperai-dev-Upscaler",
|
|
||||||
"description": "This is Flux.1-dev ControlNet for low resolution images developed by Jasper research team.",
|
|
||||||
"reference": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Upscaler",
|
|
||||||
"filename": "diffusion_pytorch_model.safetensors",
|
|
||||||
"url": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Upscaler/resolve/main/diffusion_pytorch_model.safetensors",
|
|
||||||
"size": "3.58GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "jasperai/FLUX.1-dev-Controlnet-Depth",
|
|
||||||
"type": "controlnet",
|
|
||||||
"base": "FLUX.1",
|
|
||||||
"save_path": "controlnet/FLUX.1/jasperai-dev-Depth",
|
|
||||||
"description": "This is Flux.1-dev ControlNet for Depth map developed by Jasper research team.",
|
|
||||||
"reference": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Depth",
|
|
||||||
"filename": "diffusion_pytorch_model.safetensors",
|
|
||||||
"url": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Depth/resolve/main/diffusion_pytorch_model.safetensors",
|
|
||||||
"size": "3.58GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "jasperai/Flux.1-dev-Controlnet-Surface-Normals",
|
|
||||||
"type": "controlnet",
|
|
||||||
"base": "FLUX.1",
|
|
||||||
"save_path": "controlnet/FLUX.1/jasperai-dev-Surface-Normals",
|
|
||||||
"description": "This is Flux.1-dev ControlNet for Surface Normals map developed by Jasper research team.",
|
|
||||||
"reference": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Surface-Normals",
|
|
||||||
"filename": "diffusion_pytorch_model.safetensors",
|
|
||||||
"url": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Surface-Normals/resolve/main/diffusion_pytorch_model.safetensors",
|
|
||||||
"size": "3.58GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro (fp8_e4m3fn) by Kijai",
|
|
||||||
"type": "controlnet",
|
|
||||||
"base": "FLUX.1",
|
|
||||||
"save_path": "controlnet/FLUX.1",
|
|
||||||
"description": "FLUX.1 [Dev] Union Controlnet. Supports Canny, Tile, Depth, Blur, Pose, Gray, Low Quality\nVersion quantized to fp8_e4m3fn by Kijai",
|
|
||||||
"reference": "https://huggingface.co/Kijai/flux-fp8",
|
|
||||||
"filename": "flux_shakker_labs_union_pro-fp8_e4m3fn.safetensors",
|
|
||||||
"url": "https://huggingface.co/Kijai/flux-fp8/resolve/main/flux_shakker_labs_union_pro-fp8_e4m3fn.safetensors",
|
|
||||||
"size": "3.3GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors [Long CLIP L]",
|
|
||||||
"type": "clip",
|
|
||||||
"base": "clip",
|
|
||||||
"save_path": "clip/long_clip",
|
|
||||||
"description": "Greatly improved TEXT + Detail (as CLIP-L for Flux.1)",
|
|
||||||
"reference": "https://huggingface.co/zer0int",
|
|
||||||
"filename": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors",
|
|
||||||
"url": "https://huggingface.co/zer0int/CLIP-GmP-ViT-L-14/resolve/main/ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors",
|
|
||||||
"size": "931MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors [Long CLIP L]",
|
|
||||||
"type": "clip",
|
|
||||||
"base": "clip",
|
|
||||||
"save_path": "clip/long_clip",
|
|
||||||
"description": "Greatly improved TEXT + Detail (as CLIP-L for Flux.1)",
|
|
||||||
"reference": "https://huggingface.co/zer0int",
|
|
||||||
"filename": "ViT-L-14-TEXT-detail-improved-hiT-GmP-TE-only-HF.safetensors",
|
|
||||||
"url": "https://huggingface.co/zer0int/CLIP-GmP-ViT-L-14/resolve/main/ViT-L-14-TEXT-detail-improved-hiT-GmP-TE-only-HF.safetensors",
|
|
||||||
"size": "323MB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro",
|
|
||||||
"type": "controlnet",
|
|
||||||
"base": "FLUX.1",
|
|
||||||
"save_path": "controlnet/FLUX.1/Shakker-Labs-ControlNet-Union-Pro",
|
|
||||||
"description": "FLUX.1 [Dev] Union Controlnet. Supports Canny, Tile, Depth, Blur, Pose, Gray, Low Quality",
|
|
||||||
"reference": "https://huggingface.co/Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro",
|
|
||||||
"filename": "diffusion_pytorch_model.safetensors",
|
|
||||||
"url": "https://huggingface.co/Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro/resolve/main/diffusion_pytorch_model.safetensors",
|
|
||||||
"size": "6.6GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (8steps) - FLUX.1 [Dev]",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "FLUX.1",
|
|
||||||
"save_path": "loras/HyperSD/FLUX.1",
|
|
||||||
"description": "Hyper-SD LoRA (8steps) - FLUX.1 [Dev]",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-FLUX.1-dev-8steps-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-FLUX.1-dev-8steps-lora.safetensors",
|
|
||||||
"size": "1.39GB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (16steps) - FLUX.1 [Dev]",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "FLUX.1",
|
|
||||||
"save_path": "loras/HyperSD/FLUX.1",
|
|
||||||
"description": "Hyper-SD LoRA (16steps) - FLUX.1 [Dev]",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-FLUX.1-dev-16steps-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-FLUX.1-dev-16steps-lora.safetensors",
|
|
||||||
"size": "1.39GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "BAAI/SegGPT",
|
|
||||||
"type": "SegGPT",
|
|
||||||
"base": "SegGPT",
|
|
||||||
"save_path": "custom_nodes/comfyui-SegGPT",
|
|
||||||
"description": "SegGPT",
|
|
||||||
"reference": "https://huggingface.co/BAAI/SegGPT",
|
|
||||||
"filename": "seggpt_vit_large.pth",
|
|
||||||
"url": "https://huggingface.co/BAAI/SegGPT/resolve/main/seggpt_vit_large.pth",
|
|
||||||
"size": "1.48GB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "DMD2 LoRA (4steps)",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "loras/DMD2",
|
|
||||||
"description": "DMD2 LoRA (4steps)",
|
|
||||||
"reference": "https://huggingface.co/tianweiy/DMD2",
|
|
||||||
"filename": "dmd2_sdxl_4step_lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/tianweiy/DMD2/resolve/main/dmd2_sdxl_4step_lora.safetensors",
|
|
||||||
"size": "787MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "DMD2 LoRA (4steps/fp16)",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "loras/DMD2",
|
|
||||||
"description": "DMD2 LoRA (4steps/fp16)",
|
|
||||||
"reference": "https://huggingface.co/tianweiy/DMD2",
|
|
||||||
"filename": "dmd2_sdxl_4step_lora_fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/tianweiy/DMD2/resolve/main/dmd2_sdxl_4step_lora_fp16.safetensors",
|
|
||||||
"size": "394MB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (1step) - SD1.5",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "loras/HyperSD/SD15",
|
|
||||||
"description": "Hyper-SD LoRA (1step) - SD1.5",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SD15-1step-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-1step-lora.safetensors",
|
|
||||||
"size": "269MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (2steps) - SD1.5",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "loras/HyperSD/SD15",
|
|
||||||
"description": "Hyper-SD LoRA (2steps) - SD1.5",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SD15-2steps-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-2steps-lora.safetensors",
|
|
||||||
"size": "269MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (4steps) - SD1.5",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "loras/HyperSD/SD15",
|
|
||||||
"description": "Hyper-SD LoRA (4steps)",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SD15-4steps-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-4steps-lora.safetensors",
|
|
||||||
"size": "269MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (8steps) - SD1.5",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "loras/HyperSD/SD15",
|
|
||||||
"description": "Hyper-SD LoRA (8steps)",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SD15-8steps-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-8steps-lora.safetensors",
|
|
||||||
"size": "269MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD CFG LoRA (8steps) - SD1.5",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "loras/HyperSD/SD15",
|
|
||||||
"description": "Hyper-SD CFG LoRA (8steps)",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SD15-8steps-CFG-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-8steps-CFG-lora.safetensors",
|
|
||||||
"size": "269MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD CFG LoRA (12steps) - SD1.5",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SD1.5",
|
|
||||||
"save_path": "loras/HyperSD/SD15",
|
|
||||||
"description": "Hyper-SD CFG LoRA (12steps)",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SD15-12steps-CFG-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-12steps-CFG-lora.safetensors",
|
|
||||||
"size": "269MB"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (1step) - SDXL",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "loras/HyperSD/SDXL",
|
|
||||||
"description": "Hyper-SD LoRA (1step) - SDXL",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SDXL-1step-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SDXL-1step-lora.safetensors",
|
|
||||||
"size": "787MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (2steps) - SDXL",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "loras/HyperSD/SDXL",
|
|
||||||
"description": "Hyper-SD LoRA (2steps) - SDXL",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SDXL-2steps-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SDXL-2steps-lora.safetensors",
|
|
||||||
"size": "787MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (4steps) - SDXL",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "loras/HyperSD/SDXL",
|
|
||||||
"description": "Hyper-SD LoRA (4steps) - SDXL",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SD15-4steps-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-4steps-lora.safetensors",
|
|
||||||
"size": "787MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD LoRA (8steps) - SDXL",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "loras/HyperSD/SDXL",
|
|
||||||
"description": "Hyper-SD LoRA (8steps) - SDXL",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SDXL-8steps-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SDXL-8steps-lora.safetensors",
|
|
||||||
"size": "787MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD CFG LoRA (8steps) - SDXL",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "loras/HyperSD/SDXL",
|
|
||||||
"description": "Hyper-SD CFG LoRA (8steps) - SDXL",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SDXL-8steps-CFG-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SDXL-8steps-CFG-lora.safetensors",
|
|
||||||
"size": "787MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hyper-SD CFG LoRA (12steps) - SDXL",
|
|
||||||
"type": "lora",
|
|
||||||
"base": "SDXL",
|
|
||||||
"save_path": "loras/HyperSD/SDXL",
|
|
||||||
"description": "Hyper-SD CFG LoRA (12steps) - SDXL",
|
|
||||||
"reference": "https://huggingface.co/ByteDance/Hyper-SD",
|
|
||||||
"filename": "Hyper-SDXL-12steps-CFG-lora.safetensors",
|
|
||||||
"url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SDXL-12steps-CFG-lora.safetensors",
|
|
||||||
"size": "787MB"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
"custom_nodes": [
|
"custom_nodes": [
|
||||||
|
{
|
||||||
|
"author": "Comfy-Org",
|
||||||
|
"title": "ComfyUI React Extension Template",
|
||||||
|
"reference": "https://github.com/Comfy-Org/ComfyUI-React-Extension-Template",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/Comfy-Org/ComfyUI-React-Extension-Template"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "A minimal template for creating React/TypeScript frontend extensions for ComfyUI, with complete boilerplate setup including internationalization and unit testing."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"author": "Suzie1",
|
"author": "Suzie1",
|
||||||
"title": "Guide To Making Custom Nodes in ComfyUI",
|
"title": "Guide To Making Custom Nodes in ComfyUI",
|
||||||
@@ -291,6 +301,36 @@
|
|||||||
],
|
],
|
||||||
"install_type": "git-clone",
|
"install_type": "git-clone",
|
||||||
"description": "Example of using ComfyUI Toolbar to Toggle ComfyUI links on/off"
|
"description": "Example of using ComfyUI Toolbar to Toggle ComfyUI links on/off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "xhiroga",
|
||||||
|
"title": "ComfyUI-TypeScript-CustomNode",
|
||||||
|
"reference": "https://github.com/xhiroga/ComfyUI-TypeScript-CustomNode",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/xhiroga/ComfyUI-TypeScript-CustomNode"
|
||||||
|
],
|
||||||
|
"install_type": "git-clone",
|
||||||
|
"description": "This project is generated from xhiroga/ComfyUI-TypeScript-CustomNode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "zentrocdot",
|
||||||
|
"title": "ComfyUI-Turtle_Graphics_Demos",
|
||||||
|
"reference": "https://github.com/zentrocdot/ComfyUI-Turtle_Graphics_Demo",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/zentrocdot/ComfyUI-Turtle_Graphics_Demo"
|
||||||
|
],
|
||||||
|
"description": "ComfyUI node for creating some Turtle Graphic demos.",
|
||||||
|
"install_type": "git-clone"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": "cozy-comfyui",
|
||||||
|
"title": "cozy_ex_dynamic",
|
||||||
|
"reference": "https://github.com/cozy-comfyui/cozy_ex_dynamic",
|
||||||
|
"files": [
|
||||||
|
"https://github.com/cozy-comfyui/cozy_ex_dynamic"
|
||||||
|
],
|
||||||
|
"description": "Dynamic Node examples for ComfyUI",
|
||||||
|
"install_type": "git-clone"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import atexit
|
import atexit
|
||||||
@@ -9,6 +10,7 @@ import platform
|
|||||||
import json
|
import json
|
||||||
import ast
|
import ast
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
|
|
||||||
glob_path = os.path.join(os.path.dirname(__file__), "glob")
|
glob_path = os.path.join(os.path.dirname(__file__), "glob")
|
||||||
sys.path.append(glob_path)
|
sys.path.append(glob_path)
|
||||||
@@ -19,21 +21,27 @@ import cm_global
|
|||||||
import manager_downloader
|
import manager_downloader
|
||||||
import folder_paths
|
import folder_paths
|
||||||
|
|
||||||
try:
|
manager_util.add_python_path_to_env()
|
||||||
from datetime import datetime
|
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
if hasattr(dt, 'datetime'):
|
||||||
|
from datetime import datetime as dt_datetime
|
||||||
|
|
||||||
def current_timestamp():
|
def current_timestamp():
|
||||||
return datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
return dt_datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
||||||
except:
|
else:
|
||||||
|
# NOTE: Occurs in some Mac environments.
|
||||||
import time
|
import time
|
||||||
import datetime
|
logging.error(f"[ComfyUI-Manager] fallback timestamp mode\n datetime module is invalid: '{dt.__file__}'")
|
||||||
logging.error(f"[ComfyUI-Manager] fallback timestamp mode\n datetime module is invalid: '{datetime.__file__}'")
|
|
||||||
def current_timestamp():
|
def current_timestamp():
|
||||||
return str(time.time()).split('.')[0]
|
return str(time.time()).split('.')[0]
|
||||||
|
|
||||||
security_check.security_check()
|
security_check.security_check()
|
||||||
|
|
||||||
cm_global.pip_blacklist = ['torch', 'torchsde', 'torchvision']
|
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
|
||||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||||
|
|
||||||
|
|
||||||
def skip_pip_spam(x):
|
def skip_pip_spam(x):
|
||||||
@@ -56,26 +64,19 @@ def is_import_failed_extension(name):
|
|||||||
return name in import_failed_extensions
|
return name in import_failed_extensions
|
||||||
|
|
||||||
|
|
||||||
def check_file_logging():
|
|
||||||
global enable_file_logging
|
|
||||||
try:
|
|
||||||
import configparser
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
config.read(manager_config_path)
|
|
||||||
default_conf = config['default']
|
|
||||||
|
|
||||||
if 'file_logging' in default_conf and default_conf['file_logging'].lower() == 'false':
|
|
||||||
enable_file_logging = False
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
check_file_logging()
|
|
||||||
|
|
||||||
comfy_path = os.environ.get('COMFYUI_PATH')
|
comfy_path = os.environ.get('COMFYUI_PATH')
|
||||||
|
comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
|
||||||
|
|
||||||
|
if comfy_path is None:
|
||||||
|
# legacy env var
|
||||||
|
comfy_path = os.environ.get('COMFYUI_PATH')
|
||||||
|
|
||||||
if comfy_path is None:
|
if comfy_path is None:
|
||||||
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
|
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
|
||||||
|
|
||||||
|
if comfy_base_path is None:
|
||||||
|
comfy_base_path = comfy_path
|
||||||
|
|
||||||
sys.__comfyui_manager_register_message_collapse = register_message_collapse
|
sys.__comfyui_manager_register_message_collapse = register_message_collapse
|
||||||
sys.__comfyui_manager_is_import_failed_extension = is_import_failed_extension
|
sys.__comfyui_manager_is_import_failed_extension = is_import_failed_extension
|
||||||
cm_global.register_api('cm.register_message_collapse', register_message_collapse)
|
cm_global.register_api('cm.register_message_collapse', register_message_collapse)
|
||||||
@@ -87,18 +88,58 @@ comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
|
|||||||
custom_nodes_base_path = folder_paths.get_folder_paths('custom_nodes')[0]
|
custom_nodes_base_path = folder_paths.get_folder_paths('custom_nodes')[0]
|
||||||
manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'ComfyUI-Manager'))
|
manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'ComfyUI-Manager'))
|
||||||
manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json")
|
manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json")
|
||||||
|
manager_pip_blacklist_path = os.path.join(manager_files_path, "pip_blacklist.list")
|
||||||
restore_snapshot_path = os.path.join(manager_files_path, "startup-scripts", "restore-snapshot.json")
|
restore_snapshot_path = os.path.join(manager_files_path, "startup-scripts", "restore-snapshot.json")
|
||||||
manager_config_path = os.path.join(manager_files_path, 'config.ini')
|
manager_config_path = os.path.join(manager_files_path, 'config.ini')
|
||||||
|
|
||||||
cm_cli_path = os.path.join(comfyui_manager_path, "cm-cli.py")
|
cm_cli_path = os.path.join(comfyui_manager_path, "cm-cli.py")
|
||||||
|
|
||||||
|
|
||||||
cm_global.pip_overrides = {'numpy': 'numpy<2', 'ultralytics': 'ultralytics==8.3.40'}
|
default_conf = {}
|
||||||
|
|
||||||
|
def read_config():
|
||||||
|
global default_conf
|
||||||
|
try:
|
||||||
|
import configparser
|
||||||
|
config = configparser.ConfigParser(strict=False)
|
||||||
|
config.read(manager_config_path)
|
||||||
|
default_conf = config['default']
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_uv_mode():
|
||||||
|
if 'use_uv' in default_conf:
|
||||||
|
manager_util.use_uv = default_conf['use_uv'].lower() == 'true'
|
||||||
|
|
||||||
|
def check_file_logging():
|
||||||
|
global enable_file_logging
|
||||||
|
if 'file_logging' in default_conf and default_conf['file_logging'].lower() == 'false':
|
||||||
|
enable_file_logging = False
|
||||||
|
|
||||||
|
|
||||||
|
read_config()
|
||||||
|
read_uv_mode()
|
||||||
|
check_file_logging()
|
||||||
|
|
||||||
|
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)
|
||||||
cm_global.pip_overrides['numpy'] = 'numpy<2'
|
|
||||||
cm_global.pip_overrides['ultralytics'] = 'ultralytics==8.3.40' # for security
|
if sys.version_info < (3, 13):
|
||||||
|
cm_global.pip_overrides['numpy'] = 'numpy<2'
|
||||||
|
|
||||||
|
|
||||||
|
if os.path.exists(manager_pip_blacklist_path):
|
||||||
|
with open(manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
|
||||||
|
for x in f.readlines():
|
||||||
|
y = x.strip()
|
||||||
|
if y != '':
|
||||||
|
cm_global.pip_blacklist.add(y)
|
||||||
|
|
||||||
|
|
||||||
def remap_pip_package(pkg):
|
def remap_pip_package(pkg):
|
||||||
@@ -146,6 +187,48 @@ def process_wrap(cmd_str, cwd_path, handler=None, env=None):
|
|||||||
return process.wait()
|
return process.wait()
|
||||||
|
|
||||||
|
|
||||||
|
original_stdout = sys.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def try_get_custom_nodes(x):
|
||||||
|
for custom_nodes_dir in folder_paths.get_folder_paths('custom_nodes'):
|
||||||
|
if x.startswith(custom_nodes_dir):
|
||||||
|
relative_path = os.path.relpath(x, custom_nodes_dir)
|
||||||
|
next_segment = relative_path.split(os.sep)[0]
|
||||||
|
if next_segment.lower() != 'comfyui-manager':
|
||||||
|
return next_segment, os.path.join(custom_nodes_dir, next_segment)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_origin_module():
|
||||||
|
stack = traceback.extract_stack()[:-2]
|
||||||
|
for frame in reversed(stack):
|
||||||
|
info = try_get_custom_nodes(frame.filename)
|
||||||
|
if info is None:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return info
|
||||||
|
return None
|
||||||
|
|
||||||
|
def extract_origin_module_from_strings(file_paths):
|
||||||
|
for filepath in file_paths:
|
||||||
|
info = try_get_custom_nodes(filepath)
|
||||||
|
if info is None:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return info
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def finalize_startup():
|
||||||
|
res = {}
|
||||||
|
for k, v in cm_global.error_dict.items():
|
||||||
|
if v['path'] in import_failed_extensions:
|
||||||
|
res[k] = v
|
||||||
|
|
||||||
|
cm_global.error_dict = res
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if '--port' in sys.argv:
|
if '--port' in sys.argv:
|
||||||
port_index = sys.argv.index('--port')
|
port_index = sys.argv.index('--port')
|
||||||
@@ -162,6 +245,9 @@ try:
|
|||||||
if enable_file_logging:
|
if enable_file_logging:
|
||||||
log_path_base = os.path.join(folder_paths.user_directory, 'comfyui')
|
log_path_base = os.path.join(folder_paths.user_directory, 'comfyui')
|
||||||
|
|
||||||
|
if not os.path.exists(folder_paths.user_directory):
|
||||||
|
os.makedirs(folder_paths.user_directory)
|
||||||
|
|
||||||
if os.path.exists(f"{log_path_base}{postfix}.log"):
|
if os.path.exists(f"{log_path_base}{postfix}.log"):
|
||||||
if os.path.exists(f"{log_path_base}{postfix}.prev.log"):
|
if os.path.exists(f"{log_path_base}{postfix}.prev.log"):
|
||||||
if os.path.exists(f"{log_path_base}{postfix}.prev2.log"):
|
if os.path.exists(f"{log_path_base}{postfix}.prev2.log"):
|
||||||
@@ -225,8 +311,16 @@ try:
|
|||||||
if match:
|
if match:
|
||||||
import_failed_extensions.add(match.group(1).strip())
|
import_failed_extensions.add(match.group(1).strip())
|
||||||
|
|
||||||
if 'Starting server' in message:
|
if not self.is_stdout:
|
||||||
is_start_mode = False
|
origin_info = extract_origin_module()
|
||||||
|
if origin_info is not None:
|
||||||
|
name, origin_path = origin_info
|
||||||
|
|
||||||
|
if name != 'comfyui-manager':
|
||||||
|
if name not in cm_global.error_dict:
|
||||||
|
cm_global.error_dict[name] = {'name': name, 'path': origin_path, 'msg': ''}
|
||||||
|
|
||||||
|
cm_global.error_dict[name]['msg'] += message
|
||||||
|
|
||||||
if not self.is_stdout:
|
if not self.is_stdout:
|
||||||
match = re.search(pat_tqdm, message)
|
match = re.search(pat_tqdm, message)
|
||||||
@@ -311,12 +405,34 @@ try:
|
|||||||
if match:
|
if match:
|
||||||
import_failed_extensions.add(match.group(1).strip())
|
import_failed_extensions.add(match.group(1).strip())
|
||||||
|
|
||||||
|
if 'Traceback' in message:
|
||||||
|
file_lists = self._extract_file_paths(message)
|
||||||
|
origin_info = extract_origin_module_from_strings(file_lists)
|
||||||
|
if origin_info is not None:
|
||||||
|
name, origin_path = origin_info
|
||||||
|
|
||||||
|
if name != 'comfyui-manager':
|
||||||
|
if name not in cm_global.error_dict:
|
||||||
|
cm_global.error_dict[name] = {'name': name, 'path': origin_path, 'msg': ''}
|
||||||
|
|
||||||
|
cm_global.error_dict[name]['msg'] += message
|
||||||
|
|
||||||
if 'Starting server' in message:
|
if 'Starting server' in message:
|
||||||
is_start_mode = False
|
is_start_mode = False
|
||||||
|
finalize_startup()
|
||||||
|
|
||||||
if stderr_wrapper:
|
if stderr_wrapper:
|
||||||
stderr_wrapper.sync_write(message+'\n', file_only=True)
|
stderr_wrapper.sync_write(message+'\n', file_only=True)
|
||||||
|
|
||||||
|
def _extract_file_paths(self, msg):
|
||||||
|
file_paths = []
|
||||||
|
for line in msg.split('\n'):
|
||||||
|
match = re.findall(r'File \"(.*?)\", line \d+', line)
|
||||||
|
for x in match:
|
||||||
|
if not x.startswith('<'):
|
||||||
|
file_paths.extend(match)
|
||||||
|
return file_paths
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger().addHandler(LoggingHandler())
|
logging.getLogger().addHandler(LoggingHandler())
|
||||||
|
|
||||||
@@ -325,28 +441,33 @@ except Exception as e:
|
|||||||
print(f"[ComfyUI-Manager] Logging failed: {e}")
|
print(f"[ComfyUI-Manager] Logging failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
try:
|
def ensure_dependencies():
|
||||||
import git # noqa: F401
|
|
||||||
import toml # noqa: F401
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
my_path = os.path.dirname(__file__)
|
|
||||||
requirements_path = os.path.join(my_path, "requirements.txt")
|
|
||||||
|
|
||||||
print("## ComfyUI-Manager: installing dependencies. (GitPython)")
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.check_output([sys.executable, '-s', '-m', 'pip', 'install', '-r', requirements_path])
|
import git # noqa: F401
|
||||||
except subprocess.CalledProcessError:
|
import toml # noqa: F401
|
||||||
print("## [ERROR] ComfyUI-Manager: Attempting to reinstall dependencies using an alternative method.")
|
import rich # noqa: F401
|
||||||
try:
|
import chardet # noqa: F401
|
||||||
result = subprocess.check_output([sys.executable, '-s', '-m', 'pip', 'install', '--user', '-r', requirements_path])
|
except ModuleNotFoundError:
|
||||||
except subprocess.CalledProcessError:
|
my_path = os.path.dirname(__file__)
|
||||||
print("## [ERROR] ComfyUI-Manager: Failed to install the GitPython package in the correct Python environment. Please install it manually in the appropriate environment. (You can seek help at https://app.element.io/#/room/%23comfyui_space%3Amatrix.org)")
|
requirements_path = os.path.join(my_path, "requirements.txt")
|
||||||
|
|
||||||
try:
|
print("## ComfyUI-Manager: installing dependencies. (GitPython)")
|
||||||
print("## ComfyUI-Manager: installing dependencies done.")
|
try:
|
||||||
except:
|
subprocess.check_output(manager_util.make_pip_cmd(['install', '-r', requirements_path]))
|
||||||
# maybe we should sys.exit() here? there is at least two screens worth of error messages still being pumped after our error messages
|
except subprocess.CalledProcessError:
|
||||||
print("## [ERROR] ComfyUI-Manager: GitPython package seems to be installed, but failed to load somehow. Make sure you have a working git client installed")
|
print("## [ERROR] ComfyUI-Manager: Attempting to reinstall dependencies using an alternative method.")
|
||||||
|
try:
|
||||||
|
subprocess.check_output(manager_util.make_pip_cmd(['install', '--user', '-r', requirements_path]))
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
print("## [ERROR] ComfyUI-Manager: Failed to install the GitPython package in the correct Python environment. Please install it manually in the appropriate environment. (You can seek help at https://app.element.io/#/room/%23comfyui_space%3Amatrix.org)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("## ComfyUI-Manager: installing dependencies done.")
|
||||||
|
except:
|
||||||
|
# maybe we should sys.exit() here? there is at least two screens worth of error messages still being pumped after our error messages
|
||||||
|
print("## [ERROR] ComfyUI-Manager: GitPython package seems to be installed, but failed to load somehow. Make sure you have a working git client installed")
|
||||||
|
|
||||||
|
ensure_dependencies()
|
||||||
|
|
||||||
|
|
||||||
print("** ComfyUI startup time:", current_timestamp())
|
print("** ComfyUI startup time:", current_timestamp())
|
||||||
@@ -354,6 +475,7 @@ print("** Platform:", platform.system())
|
|||||||
print("** Python version:", sys.version)
|
print("** Python version:", sys.version)
|
||||||
print("** Python executable:", sys.executable)
|
print("** Python executable:", sys.executable)
|
||||||
print("** ComfyUI Path:", comfy_path)
|
print("** ComfyUI Path:", comfy_path)
|
||||||
|
print("** ComfyUI Base Folder Path:", comfy_base_path)
|
||||||
print("** User directory:", folder_paths.user_directory)
|
print("** User directory:", folder_paths.user_directory)
|
||||||
print("** ComfyUI-Manager config path:", manager_config_path)
|
print("** ComfyUI-Manager config path:", manager_config_path)
|
||||||
|
|
||||||
@@ -366,11 +488,6 @@ else:
|
|||||||
|
|
||||||
def read_downgrade_blacklist():
|
def read_downgrade_blacklist():
|
||||||
try:
|
try:
|
||||||
import configparser
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
config.read(manager_config_path)
|
|
||||||
default_conf = config['default']
|
|
||||||
|
|
||||||
if 'downgrade_blacklist' in default_conf:
|
if 'downgrade_blacklist' in default_conf:
|
||||||
items = default_conf['downgrade_blacklist'].split(',')
|
items = default_conf['downgrade_blacklist'].split(',')
|
||||||
items = [x.strip() for x in items if x != '']
|
items = [x.strip() for x in items if x != '']
|
||||||
@@ -385,26 +502,20 @@ read_downgrade_blacklist()
|
|||||||
|
|
||||||
def check_bypass_ssl():
|
def check_bypass_ssl():
|
||||||
try:
|
try:
|
||||||
import configparser
|
|
||||||
import ssl
|
import ssl
|
||||||
config = configparser.ConfigParser()
|
|
||||||
config.read(manager_config_path)
|
|
||||||
default_conf = config['default']
|
|
||||||
|
|
||||||
if 'bypass_ssl' in default_conf and default_conf['bypass_ssl'].lower() == 'true':
|
if 'bypass_ssl' in default_conf and default_conf['bypass_ssl'].lower() == 'true':
|
||||||
print(f"[ComfyUI-Manager] WARN: Unsafe - SSL verification bypass option is Enabled. (see {manager_config_path})")
|
print(f"[ComfyUI-Manager] WARN: Unsafe - SSL verification bypass option is Enabled. (see {manager_config_path})")
|
||||||
ssl._create_default_https_context = ssl._create_unverified_context # SSL certificate error fix.
|
ssl._create_default_https_context = ssl._create_unverified_context # SSL certificate error fix.
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
check_bypass_ssl()
|
check_bypass_ssl()
|
||||||
|
|
||||||
|
|
||||||
# Perform install
|
# Perform install
|
||||||
processed_install = set()
|
processed_install = set()
|
||||||
script_list_path = os.path.join(folder_paths.user_directory, "default", "ComfyUI-Manager", "startup-scripts", "install-scripts.txt")
|
script_list_path = os.path.join(folder_paths.user_directory, "default", "ComfyUI-Manager", "startup-scripts", "install-scripts.txt")
|
||||||
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages())
|
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
|
||||||
|
|
||||||
|
|
||||||
def is_installed(name):
|
def is_installed(name):
|
||||||
@@ -485,7 +596,8 @@ if os.path.exists(restore_snapshot_path):
|
|||||||
|
|
||||||
print("[ComfyUI-Manager] Restore snapshot.")
|
print("[ComfyUI-Manager] Restore snapshot.")
|
||||||
new_env = os.environ.copy()
|
new_env = os.environ.copy()
|
||||||
new_env["COMFYUI_PATH"] = comfy_path
|
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
|
||||||
|
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
|
||||||
|
|
||||||
cmd_str = [sys.executable, cm_cli_path, 'restore-snapshot', restore_snapshot_path]
|
cmd_str = [sys.executable, cm_cli_path, 'restore-snapshot', restore_snapshot_path]
|
||||||
exit_code = process_wrap(cmd_str, custom_nodes_base_path, handler=msg_capture, env=new_env)
|
exit_code = process_wrap(cmd_str, custom_nodes_base_path, handler=msg_capture, env=new_env)
|
||||||
@@ -510,17 +622,19 @@ def execute_lazy_install_script(repo_path, executable):
|
|||||||
|
|
||||||
if os.path.exists(requirements_path):
|
if os.path.exists(requirements_path):
|
||||||
print(f"Install: pip packages for '{repo_path}'")
|
print(f"Install: pip packages for '{repo_path}'")
|
||||||
with open(requirements_path, "r") as requirements_file:
|
|
||||||
for line in requirements_file:
|
|
||||||
package_name = remap_pip_package(line.strip())
|
|
||||||
if package_name and not is_installed(package_name):
|
|
||||||
if '--index-url' in package_name:
|
|
||||||
s = package_name.split('--index-url')
|
|
||||||
install_cmd = [sys.executable, "-m", "pip", "install", s[0].strip(), '--index-url', s[1].strip()]
|
|
||||||
else:
|
|
||||||
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
|
|
||||||
|
|
||||||
process_wrap(install_cmd, repo_path)
|
lines = manager_util.robust_readlines(requirements_path)
|
||||||
|
for line in lines:
|
||||||
|
package_name = remap_pip_package(line.strip())
|
||||||
|
package_name = package_name.split('#')[0].strip()
|
||||||
|
if package_name and not is_installed(package_name):
|
||||||
|
if '--index-url' in package_name:
|
||||||
|
s = package_name.split('--index-url')
|
||||||
|
install_cmd = manager_util.make_pip_cmd(["install", s[0].strip(), '--index-url', s[1].strip()])
|
||||||
|
else:
|
||||||
|
install_cmd = manager_util.make_pip_cmd(["install", package_name])
|
||||||
|
|
||||||
|
process_wrap(install_cmd, repo_path)
|
||||||
|
|
||||||
if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install:
|
if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install:
|
||||||
processed_install.add(f'{repo_path}/install.py')
|
processed_install.add(f'{repo_path}/install.py')
|
||||||
@@ -528,7 +642,8 @@ def execute_lazy_install_script(repo_path, executable):
|
|||||||
install_cmd = [executable, "install.py"]
|
install_cmd = [executable, "install.py"]
|
||||||
|
|
||||||
new_env = os.environ.copy()
|
new_env = os.environ.copy()
|
||||||
new_env["COMFYUI_PATH"] = comfy_path
|
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
|
||||||
|
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
|
||||||
process_wrap(install_cmd, repo_path, env=new_env)
|
process_wrap(install_cmd, repo_path, env=new_env)
|
||||||
|
|
||||||
|
|
||||||
@@ -583,19 +698,43 @@ def execute_lazy_cnr_switch(target, zip_url, from_path, to_path, no_deps, custom
|
|||||||
file.write('\n'.join(list(extracted)))
|
file.write('\n'.join(list(extracted)))
|
||||||
|
|
||||||
|
|
||||||
def execute_migration(moves):
|
script_executed = False
|
||||||
import shutil
|
|
||||||
for x in moves:
|
|
||||||
if os.path.exists(x[0]) and not os.path.exists(x[1]):
|
|
||||||
shutil.move(x[0], x[1])
|
|
||||||
print(f"[ComfyUI-Manager] MIGRATION: '{x[0]}' -> '{x[1]}'")
|
|
||||||
|
|
||||||
|
def execute_startup_script():
|
||||||
# Check if script_list_path exists
|
global script_executed
|
||||||
if os.path.exists(script_list_path):
|
|
||||||
print("\n#######################################################################")
|
print("\n#######################################################################")
|
||||||
print("[ComfyUI-Manager] Starting dependency installation/(de)activation for the extension\n")
|
print("[ComfyUI-Manager] Starting dependency installation/(de)activation for the extension\n")
|
||||||
|
|
||||||
|
custom_nodelist_cache = None
|
||||||
|
|
||||||
|
def get_custom_node_paths():
|
||||||
|
nonlocal custom_nodelist_cache
|
||||||
|
if custom_nodelist_cache is None:
|
||||||
|
custom_nodelist_cache = set()
|
||||||
|
for base in folder_paths.get_folder_paths('custom_nodes'):
|
||||||
|
for x in os.listdir(base):
|
||||||
|
fullpath = os.path.join(base, x)
|
||||||
|
if os.path.isdir(fullpath):
|
||||||
|
custom_nodelist_cache.add(fullpath)
|
||||||
|
|
||||||
|
return custom_nodelist_cache
|
||||||
|
|
||||||
|
def execute_lazy_delete(path):
|
||||||
|
# Validate to prevent arbitrary paths from being deleted
|
||||||
|
if path not in get_custom_node_paths():
|
||||||
|
logging.error(f"## ComfyUI-Manager: The scheduled '{path}' is not a custom node path, so the deletion has been canceled.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not os.path.exists(path):
|
||||||
|
logging.info(f"## ComfyUI-Manager: SKIP-DELETE => '{path}' (already deleted)")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(path)
|
||||||
|
logging.info(f"## ComfyUI-Manager: DELETE => '{path}'")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"## ComfyUI-Manager: Failed to delete '{path}' ({e})")
|
||||||
|
|
||||||
executed = set()
|
executed = set()
|
||||||
# Read each line from the file and convert it to a list using eval
|
# Read each line from the file and convert it to a list using eval
|
||||||
with open(script_list_path, 'r', encoding="UTF-8", errors="ignore") as file:
|
with open(script_list_path, 'r', encoding="UTF-8", errors="ignore") as file:
|
||||||
@@ -616,8 +755,8 @@ if os.path.exists(script_list_path):
|
|||||||
execute_lazy_cnr_switch(script[0], script[2], script[3], script[4], script[5], script[6])
|
execute_lazy_cnr_switch(script[0], script[2], script[3], script[4], script[5], script[6])
|
||||||
execute_lazy_install_script(script[3], script[7])
|
execute_lazy_install_script(script[3], script[7])
|
||||||
|
|
||||||
elif script[1] == "#LAZY-MIGRATION":
|
elif script[1] == "#LAZY-DELETE-NODEPACK":
|
||||||
execute_migration(script[2])
|
execute_lazy_delete(script[2])
|
||||||
|
|
||||||
elif os.path.exists(script[0]):
|
elif os.path.exists(script[0]):
|
||||||
if script[1] == "#FORCE":
|
if script[1] == "#FORCE":
|
||||||
@@ -627,38 +766,72 @@ if os.path.exists(script_list_path):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"\n## ComfyUI-Manager: EXECUTE => {script[1:]}")
|
print(f"\n## ComfyUI-Manager: EXECUTE => {script[1:]}")
|
||||||
print(f"\n## Execute install/(de)activation script for '{script[0]}'")
|
print(f"\n## Execute management script for '{script[0]}'")
|
||||||
|
|
||||||
new_env = os.environ.copy()
|
new_env = os.environ.copy()
|
||||||
new_env["COMFYUI_PATH"] = comfy_path
|
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
|
||||||
|
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
|
||||||
exit_code = process_wrap(script[1:], script[0], env=new_env)
|
exit_code = process_wrap(script[1:], script[0], env=new_env)
|
||||||
|
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
print(f"install/(de)activation script failed: {script[0]}")
|
print(f"management script failed: {script[0]}")
|
||||||
else:
|
else:
|
||||||
print(f"\n## ComfyUI-Manager: CANCELED => {script[1:]}")
|
print(f"\n## ComfyUI-Manager: CANCELED => {script[1:]}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Failed to execute install/(de)activation script: {line} / {e}")
|
print(f"[ERROR] Failed to execute management script: {line} / {e}")
|
||||||
|
|
||||||
# Remove the script_list_path file
|
# Remove the script_list_path file
|
||||||
if os.path.exists(script_list_path):
|
if os.path.exists(script_list_path):
|
||||||
|
script_executed = True
|
||||||
os.remove(script_list_path)
|
os.remove(script_list_path)
|
||||||
|
|
||||||
print("\n[ComfyUI-Manager] Startup script completed.")
|
print("\n[ComfyUI-Manager] Startup script completed.")
|
||||||
print("#######################################################################\n")
|
print("#######################################################################\n")
|
||||||
|
|
||||||
|
|
||||||
|
# Check if script_list_path exists
|
||||||
|
if os.path.exists(script_list_path):
|
||||||
|
execute_startup_script()
|
||||||
|
|
||||||
|
|
||||||
pip_fixer.fix_broken()
|
pip_fixer.fix_broken()
|
||||||
|
|
||||||
del processed_install
|
del processed_install
|
||||||
del pip_fixer
|
del pip_fixer
|
||||||
manager_util.clear_pip_cache()
|
manager_util.clear_pip_cache()
|
||||||
|
|
||||||
|
if script_executed:
|
||||||
|
# Restart
|
||||||
|
print("[ComfyUI-Manager] Restarting to reapply dependency installation.")
|
||||||
|
|
||||||
|
if '__COMFY_CLI_SESSION__' in os.environ:
|
||||||
|
with open(os.path.join(os.environ['__COMFY_CLI_SESSION__'] + '.reboot'), 'w'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("--------------------------------------------------------------------------\n")
|
||||||
|
exit(0)
|
||||||
|
else:
|
||||||
|
sys_argv = sys.argv.copy()
|
||||||
|
|
||||||
|
if sys_argv[0].endswith("__main__.py"): # this is a python module
|
||||||
|
module_name = os.path.basename(os.path.dirname(sys_argv[0]))
|
||||||
|
cmds = [sys.executable, '-m', module_name] + sys_argv[1:]
|
||||||
|
elif sys.platform.startswith('win32'):
|
||||||
|
cmds = ['"' + sys.executable + '"', '"' + sys_argv[0] + '"'] + sys_argv[1:]
|
||||||
|
else:
|
||||||
|
cmds = [sys.executable] + sys_argv
|
||||||
|
|
||||||
|
print(f"Command: {cmds}", flush=True)
|
||||||
|
print("--------------------------------------------------------------------------\n")
|
||||||
|
|
||||||
|
os.execv(sys.executable, cmds)
|
||||||
|
|
||||||
|
|
||||||
def check_windows_event_loop_policy():
|
def check_windows_event_loop_policy():
|
||||||
try:
|
try:
|
||||||
import configparser
|
import configparser
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser(strict=False)
|
||||||
config.read(manager_config_path)
|
config.read(manager_config_path)
|
||||||
default_conf = config['default']
|
default_conf = config['default']
|
||||||
|
|
||||||
|
|||||||
@@ -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.7.2"
|
version = "3.32.3"
|
||||||
license = { file = "LICENSE.txt" }
|
license = { file = "LICENSE.txt" }
|
||||||
dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions"]
|
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"
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ huggingface-hub>0.20
|
|||||||
typer
|
typer
|
||||||
rich
|
rich
|
||||||
typing-extensions
|
typing-extensions
|
||||||
toml
|
toml
|
||||||
|
uv
|
||||||
|
chardet
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ python -m venv venv
|
|||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
|
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
|
||||||
python -m pip install -r requirements.txt
|
python -m pip install -r requirements.txt
|
||||||
python -m pip install -r custom_nodes/ComfyUI-Manager/requirements.txt
|
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
|
||||||
cd ..
|
cd ..
|
||||||
echo "#!/bin/bash" > run_gpu.sh
|
echo "#!/bin/bash" > run_gpu.sh
|
||||||
echo "cd ComfyUI" >> run_gpu.sh
|
echo "cd ComfyUI" >> run_gpu.sh
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ python -m venv venv
|
|||||||
call venv/Scripts/activate
|
call venv/Scripts/activate
|
||||||
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
|
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
|
||||||
python -m pip install -r requirements.txt
|
python -m pip install -r requirements.txt
|
||||||
python -m pip install -r custom_nodes/ComfyUI-Manager/requirements.txt
|
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
|
||||||
cd ..
|
cd ..
|
||||||
echo "cd ComfyUI" >> run_gpu.bat
|
echo "cd ComfyUI" >> run_gpu.bat
|
||||||
echo "call venv/Scripts/activate" >> run_gpu.bat
|
echo "call venv/Scripts/activate" >> run_gpu.bat
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
.\python_embeded\python.exe -s -m pip install gitpython
|
.\python_embeded\python.exe -s -m pip install gitpython
|
||||||
.\python_embeded\python.exe -c "import git; git.Repo.clone_from('https://github.com/ltdrdata/ComfyUI-Manager', './ComfyUI/custom_nodes/comfyui-manager')"
|
.\python_embeded\python.exe -c "import git; git.Repo.clone_from('https://github.com/ltdrdata/ComfyUI-Manager', './ComfyUI/custom_nodes/comfyui-manager')"
|
||||||
|
.\python_embeded\python.exe -m pip install -r ./ComfyUI/custom_nodes/comfyui-manager/requirements.txt
|
||||||
|
|||||||
19
tests-api/.gitignore
vendored
Normal file
19
tests-api/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Python cache files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Pytest cache
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Test-specific resources
|
||||||
|
resources/tmp/
|
||||||
91
tests-api/README.md
Normal file
91
tests-api/README.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# ComfyUI-Manager API Tests
|
||||||
|
|
||||||
|
This directory contains tests for the ComfyUI-Manager API endpoints, validating the OpenAPI specification and ensuring API functionality.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Install test dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements-test.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ensure ComfyUI is running with ComfyUI-Manager installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start ComfyUI with the default server
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### Run all tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest -xvs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run specific test files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run only the spec validation tests
|
||||||
|
pytest -xvs test_spec_validation.py
|
||||||
|
|
||||||
|
# Run only the custom node API tests
|
||||||
|
pytest -xvs test_customnode_api.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run specific test functions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run a specific test
|
||||||
|
pytest -xvs test_customnode_api.py::test_get_custom_node_list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Configuration
|
||||||
|
|
||||||
|
The tests use the following default configuration:
|
||||||
|
|
||||||
|
- Server URL: `http://localhost:8188`
|
||||||
|
- Server timeout: 2 seconds
|
||||||
|
- Wait between requests: 0.5 seconds
|
||||||
|
- Maximum retries: 3
|
||||||
|
|
||||||
|
You can override these settings with environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use a different server URL
|
||||||
|
COMFYUI_SERVER_URL=http://localhost:8189 pytest -xvs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
The tests are organized into the following categories:
|
||||||
|
|
||||||
|
1. **Spec Validation** (`test_spec_validation.py`): Validates that the OpenAPI specification is correct and complete.
|
||||||
|
2. **Custom Node API** (`test_customnode_api.py`): Tests for custom node management endpoints.
|
||||||
|
3. **Snapshot API** (`test_snapshot_api.py`): Tests for snapshot management endpoints.
|
||||||
|
4. **Queue API** (`test_queue_api.py`): Tests for queue management endpoints.
|
||||||
|
5. **Config API** (`test_config_api.py`): Tests for configuration endpoints.
|
||||||
|
6. **Model API** (`test_model_api.py`): Tests for model management endpoints (minimal as these are being deprecated).
|
||||||
|
|
||||||
|
## Test Implementation Details
|
||||||
|
|
||||||
|
### Fixtures
|
||||||
|
|
||||||
|
- `test_config`: Provides the test configuration
|
||||||
|
- `server_url`: Returns the server URL from the configuration
|
||||||
|
- `openapi_spec`: Loads the OpenAPI specification
|
||||||
|
- `api_client`: Creates a requests Session for API calls
|
||||||
|
- `api_request`: Helper function for making consistent API requests
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
|
||||||
|
- `validation.py`: Functions for validating responses against the OpenAPI schema
|
||||||
|
- `schema_utils.py`: Utilities for extracting and manipulating schemas
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Some tests are skipped with `@pytest.mark.skip` to avoid modifying state in automated testing
|
||||||
|
- Security-level restricted endpoints have minimal tests to avoid security issues
|
||||||
|
- Tests focus on read operations rather than write operations where possible
|
||||||
1
tests-api/__init__.py
Normal file
1
tests-api/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Make tests-api directory a proper package
|
||||||
237
tests-api/conftest.py
Normal file
237
tests-api/conftest.py
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
"""
|
||||||
|
PyTest configuration and fixtures for API tests.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Generator, Optional, Tuple
|
||||||
|
|
||||||
|
# Import test utilities
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Get the absolute path to the current file (conftest.py)
|
||||||
|
current_file = Path(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# Get the directory containing the current file (the tests-api directory)
|
||||||
|
tests_api_dir = current_file.parent
|
||||||
|
|
||||||
|
# Add the tests-api directory to the Python path
|
||||||
|
if str(tests_api_dir) not in sys.path:
|
||||||
|
sys.path.insert(0, str(tests_api_dir))
|
||||||
|
|
||||||
|
# Apply mocks for ComfyUI imports
|
||||||
|
from mocks.patch import apply_mocks
|
||||||
|
apply_mocks()
|
||||||
|
|
||||||
|
# Now we can import from utils.validation
|
||||||
|
from utils.validation import load_openapi_spec
|
||||||
|
|
||||||
|
|
||||||
|
# Default test configuration
|
||||||
|
DEFAULT_TEST_CONFIG = {
|
||||||
|
"server_url": "http://localhost:8188",
|
||||||
|
"server_timeout": 2, # seconds
|
||||||
|
"wait_between_requests": 0.5, # seconds
|
||||||
|
"max_retries": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_config() -> Dict:
|
||||||
|
"""
|
||||||
|
Load test configuration from environment variables or use defaults.
|
||||||
|
"""
|
||||||
|
config = DEFAULT_TEST_CONFIG.copy()
|
||||||
|
|
||||||
|
# Override from environment variables if present
|
||||||
|
if "COMFYUI_SERVER_URL" in os.environ:
|
||||||
|
config["server_url"] = os.environ["COMFYUI_SERVER_URL"]
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def server_url(test_config: Dict) -> str:
|
||||||
|
"""
|
||||||
|
Get the server URL from the test configuration.
|
||||||
|
"""
|
||||||
|
return test_config["server_url"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def openapi_spec() -> Dict:
|
||||||
|
"""
|
||||||
|
Load the OpenAPI specification.
|
||||||
|
"""
|
||||||
|
return load_openapi_spec()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def api_client(server_url: str, test_config: Dict) -> requests.Session:
|
||||||
|
"""
|
||||||
|
Create a requests Session for API calls.
|
||||||
|
"""
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
# Check if the server is running
|
||||||
|
try:
|
||||||
|
response = session.get(f"{server_url}/", timeout=test_config["server_timeout"])
|
||||||
|
response.raise_for_status()
|
||||||
|
except (requests.ConnectionError, requests.Timeout, requests.HTTPError):
|
||||||
|
pytest.skip("ComfyUI server is not running or not accessible")
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def temp_dir() -> Generator[Path, None, None]:
|
||||||
|
"""
|
||||||
|
Create a temporary directory for test files.
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
yield Path(temp_dir)
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityLevelContext:
|
||||||
|
"""
|
||||||
|
Context manager for setting and restoring security levels.
|
||||||
|
"""
|
||||||
|
def __init__(self, api_client: requests.Session, server_url: str, security_level: str):
|
||||||
|
self.api_client = api_client
|
||||||
|
self.server_url = server_url
|
||||||
|
self.security_level = security_level
|
||||||
|
self.original_level = None
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
# Get the current security level (not directly exposed in API, would require more setup)
|
||||||
|
# For now, we'll just set the new level
|
||||||
|
|
||||||
|
# Set the new security level
|
||||||
|
# Note: In a real implementation, we would need a way to set this
|
||||||
|
# This is a placeholder - the actual implementation would depend on how
|
||||||
|
# security levels are managed in ComfyUI-Manager
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
# Restore the original security level if needed
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def security_level_context(api_client: requests.Session, server_url: str):
|
||||||
|
"""
|
||||||
|
Create a context manager for setting security levels.
|
||||||
|
"""
|
||||||
|
return lambda level: SecurityLevelContext(api_client, server_url, level)
|
||||||
|
|
||||||
|
|
||||||
|
def make_api_url(server_url: str, path: str) -> str:
|
||||||
|
"""
|
||||||
|
Construct a full API URL from the server URL and path.
|
||||||
|
"""
|
||||||
|
# Ensure the path starts with a slash
|
||||||
|
if not path.startswith("/"):
|
||||||
|
path = f"/{path}"
|
||||||
|
|
||||||
|
# Remove trailing slash from server_url if present
|
||||||
|
if server_url.endswith("/"):
|
||||||
|
server_url = server_url[:-1]
|
||||||
|
|
||||||
|
return f"{server_url}{path}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def api_request(api_client: requests.Session, server_url: str, test_config: Dict):
|
||||||
|
"""
|
||||||
|
Helper function for making API requests with consistent behavior.
|
||||||
|
"""
|
||||||
|
def _request(
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
params: Optional[Dict] = None,
|
||||||
|
json_data: Optional[Dict] = None,
|
||||||
|
headers: Optional[Dict] = None,
|
||||||
|
expected_status: int = 200,
|
||||||
|
retry_on_error: bool = True,
|
||||||
|
) -> Tuple[requests.Response, Optional[Dict]]:
|
||||||
|
"""
|
||||||
|
Make an API request with automatic validation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: HTTP method
|
||||||
|
path: API path
|
||||||
|
params: Query parameters
|
||||||
|
json_data: JSON request body
|
||||||
|
headers: HTTP headers
|
||||||
|
expected_status: Expected HTTP status code
|
||||||
|
retry_on_error: Whether to retry on connection errors
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (Response object, JSON response data or None)
|
||||||
|
"""
|
||||||
|
method = method.lower()
|
||||||
|
url = make_api_url(server_url, path)
|
||||||
|
|
||||||
|
if headers is None:
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
# Add common headers
|
||||||
|
headers.setdefault("Accept", "application/json")
|
||||||
|
|
||||||
|
# Sleep between requests to avoid overwhelming the server
|
||||||
|
time.sleep(test_config["wait_between_requests"])
|
||||||
|
|
||||||
|
retries = test_config["max_retries"] if retry_on_error else 0
|
||||||
|
last_exception = None
|
||||||
|
|
||||||
|
for attempt in range(retries + 1):
|
||||||
|
try:
|
||||||
|
if method == "get":
|
||||||
|
response = api_client.get(url, params=params, headers=headers)
|
||||||
|
elif method == "post":
|
||||||
|
response = api_client.post(url, params=params, json=json_data, headers=headers)
|
||||||
|
elif method == "put":
|
||||||
|
response = api_client.put(url, params=params, json=json_data, headers=headers)
|
||||||
|
elif method == "delete":
|
||||||
|
response = api_client.delete(url, params=params, headers=headers)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||||
|
|
||||||
|
# Check status code
|
||||||
|
assert response.status_code == expected_status, (
|
||||||
|
f"Expected status code {expected_status}, got {response.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse JSON response if possible
|
||||||
|
json_response = None
|
||||||
|
if response.headers.get("Content-Type", "").startswith("application/json"):
|
||||||
|
try:
|
||||||
|
json_response = response.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
if expected_status == 200:
|
||||||
|
raise ValueError("Response was not valid JSON")
|
||||||
|
|
||||||
|
return response, json_response
|
||||||
|
|
||||||
|
except (requests.ConnectionError, requests.Timeout) as e:
|
||||||
|
last_exception = e
|
||||||
|
if attempt < retries:
|
||||||
|
# Wait before retrying
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
if last_exception:
|
||||||
|
raise last_exception
|
||||||
|
|
||||||
|
raise RuntimeError("Failed to make API request")
|
||||||
|
|
||||||
|
return _request
|
||||||
1
tests-api/mocks/__init__.py
Normal file
1
tests-api/mocks/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Make tests-api/mocks directory a proper package
|
||||||
26
tests-api/mocks/custom_node_manager.py
Normal file
26
tests-api/mocks/custom_node_manager.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
Mock CustomNodeManager for testing purposes
|
||||||
|
"""
|
||||||
|
|
||||||
|
class CustomNodeManager:
|
||||||
|
"""
|
||||||
|
Mock implementation of the CustomNodeManager class
|
||||||
|
"""
|
||||||
|
instance = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.custom_nodes = {}
|
||||||
|
self.node_paths = []
|
||||||
|
self.refresh_timeout = None
|
||||||
|
|
||||||
|
def get_node_path(self, node_class):
|
||||||
|
"""
|
||||||
|
Mock implementation to get the path for a node class
|
||||||
|
"""
|
||||||
|
return self.custom_nodes.get(node_class, None)
|
||||||
|
|
||||||
|
def update_node_paths(self):
|
||||||
|
"""
|
||||||
|
Mock implementation to update node paths
|
||||||
|
"""
|
||||||
|
pass
|
||||||
116
tests-api/mocks/patch.py
Normal file
116
tests-api/mocks/patch.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
Patch module to mock imports for testing
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Import mock modules
|
||||||
|
from mocks.prompt_server import PromptServer
|
||||||
|
from mocks.custom_node_manager import CustomNodeManager
|
||||||
|
|
||||||
|
# Current directory
|
||||||
|
current_dir = Path(__file__).parent.parent # tests-api directory
|
||||||
|
|
||||||
|
# Define mocks
|
||||||
|
class MockModule:
|
||||||
|
"""Base class for mock modules"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create server mock module with PromptServer
|
||||||
|
server_mock = MockModule()
|
||||||
|
server_mock.PromptServer = PromptServer
|
||||||
|
prompt_server_instance = PromptServer()
|
||||||
|
server_mock.PromptServer.instance = prompt_server_instance
|
||||||
|
server_mock.PromptServer.inst = prompt_server_instance
|
||||||
|
|
||||||
|
# Create app mock module with custom_node_manager submodule
|
||||||
|
app_mock = MockModule()
|
||||||
|
app_custom_node_manager = MockModule()
|
||||||
|
app_custom_node_manager.CustomNodeManager = CustomNodeManager
|
||||||
|
app_custom_node_manager.CustomNodeManager.instance = CustomNodeManager()
|
||||||
|
|
||||||
|
# Create utils mock module with json_util submodule
|
||||||
|
utils_mock = MockModule()
|
||||||
|
utils_json_util = MockModule()
|
||||||
|
|
||||||
|
# Create utils.validation and utils.schema_utils submodules
|
||||||
|
utils_validation = MockModule()
|
||||||
|
utils_schema_utils = MockModule()
|
||||||
|
|
||||||
|
# Import actual modules (make sure path is set up correctly)
|
||||||
|
sys.path.insert(0, str(current_dir))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the validation module
|
||||||
|
from utils.validation import load_openapi_spec
|
||||||
|
utils_validation.load_openapi_spec = load_openapi_spec
|
||||||
|
|
||||||
|
# Import all schema_utils functions
|
||||||
|
from utils.schema_utils import (
|
||||||
|
get_all_paths,
|
||||||
|
get_grouped_paths,
|
||||||
|
get_methods_for_path,
|
||||||
|
find_paths_with_security,
|
||||||
|
get_content_types_for_response,
|
||||||
|
get_required_parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
utils_schema_utils.get_all_paths = get_all_paths
|
||||||
|
utils_schema_utils.get_grouped_paths = get_grouped_paths
|
||||||
|
utils_schema_utils.get_methods_for_path = get_methods_for_path
|
||||||
|
utils_schema_utils.find_paths_with_security = find_paths_with_security
|
||||||
|
utils_schema_utils.get_content_types_for_response = get_content_types_for_response
|
||||||
|
utils_schema_utils.get_required_parameters = get_required_parameters
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Error importing test utilities: {e}")
|
||||||
|
# Define dummy functions if imports fail
|
||||||
|
def dummy_load_openapi_spec():
|
||||||
|
"""Dummy function for testing"""
|
||||||
|
return {"paths": {}}
|
||||||
|
utils_validation.load_openapi_spec = dummy_load_openapi_spec
|
||||||
|
|
||||||
|
def dummy_get_all_paths(spec):
|
||||||
|
return list(spec.get("paths", {}).keys())
|
||||||
|
utils_schema_utils.get_all_paths = dummy_get_all_paths
|
||||||
|
|
||||||
|
def dummy_get_grouped_paths(spec):
|
||||||
|
return {}
|
||||||
|
utils_schema_utils.get_grouped_paths = dummy_get_grouped_paths
|
||||||
|
|
||||||
|
def dummy_get_methods_for_path(spec, path):
|
||||||
|
return []
|
||||||
|
utils_schema_utils.get_methods_for_path = dummy_get_methods_for_path
|
||||||
|
|
||||||
|
def dummy_find_paths_with_security(spec, security_scheme=None):
|
||||||
|
return []
|
||||||
|
utils_schema_utils.find_paths_with_security = dummy_find_paths_with_security
|
||||||
|
|
||||||
|
def dummy_get_content_types_for_response(spec, path, method, status_code="200"):
|
||||||
|
return []
|
||||||
|
utils_schema_utils.get_content_types_for_response = dummy_get_content_types_for_response
|
||||||
|
|
||||||
|
def dummy_get_required_parameters(spec, path, method):
|
||||||
|
return []
|
||||||
|
utils_schema_utils.get_required_parameters = dummy_get_required_parameters
|
||||||
|
|
||||||
|
# Add merge_json_recursive from our mock utils
|
||||||
|
from mocks.utils import merge_json_recursive
|
||||||
|
utils_json_util.merge_json_recursive = merge_json_recursive
|
||||||
|
|
||||||
|
# Apply the mocks to sys.modules
|
||||||
|
def apply_mocks():
|
||||||
|
"""Apply all mocks to sys.modules"""
|
||||||
|
sys.modules['server'] = server_mock
|
||||||
|
sys.modules['app'] = app_mock
|
||||||
|
sys.modules['app.custom_node_manager'] = app_custom_node_manager
|
||||||
|
sys.modules['utils'] = utils_mock
|
||||||
|
sys.modules['utils.json_util'] = utils_json_util
|
||||||
|
sys.modules['utils.validation'] = utils_validation
|
||||||
|
sys.modules['utils.schema_utils'] = utils_schema_utils
|
||||||
|
|
||||||
|
# Make sure our actual utils module is importable
|
||||||
|
if current_dir not in sys.path:
|
||||||
|
sys.path.insert(0, str(current_dir))
|
||||||
71
tests-api/mocks/prompt_server.py
Normal file
71
tests-api/mocks/prompt_server.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
Mock PromptServer for testing purposes
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MockRoutes:
|
||||||
|
"""
|
||||||
|
Mock routing class with method decorators
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.routes = {}
|
||||||
|
|
||||||
|
def get(self, path):
|
||||||
|
"""Decorator for GET routes"""
|
||||||
|
def decorator(f):
|
||||||
|
self.routes[('GET', path)] = f
|
||||||
|
return f
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def post(self, path):
|
||||||
|
"""Decorator for POST routes"""
|
||||||
|
def decorator(f):
|
||||||
|
self.routes[('POST', path)] = f
|
||||||
|
return f
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def put(self, path):
|
||||||
|
"""Decorator for PUT routes"""
|
||||||
|
def decorator(f):
|
||||||
|
self.routes[('PUT', path)] = f
|
||||||
|
return f
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def delete(self, path):
|
||||||
|
"""Decorator for DELETE routes"""
|
||||||
|
def decorator(f):
|
||||||
|
self.routes[('DELETE', path)] = f
|
||||||
|
return f
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class PromptServer:
|
||||||
|
"""
|
||||||
|
Mock implementation of the PromptServer class
|
||||||
|
"""
|
||||||
|
instance = None
|
||||||
|
inst = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.routes = MockRoutes()
|
||||||
|
self.registered_paths = set()
|
||||||
|
self.base_url = "http://127.0.0.1:8188" # Assuming server is running on default port
|
||||||
|
self.queue_lock = None
|
||||||
|
|
||||||
|
def add_route(self, method, path, handler, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Add a mock route to the server
|
||||||
|
"""
|
||||||
|
self.routes.routes[(method.upper(), path)] = handler
|
||||||
|
self.registered_paths.add(path)
|
||||||
|
|
||||||
|
async def send_msg(self, message, data=None):
|
||||||
|
"""
|
||||||
|
Mock send_msg method (does nothing in the mock)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_sync(self, message, data=None):
|
||||||
|
"""
|
||||||
|
Mock send_sync method (does nothing in the mock)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
20
tests-api/mocks/utils.py
Normal file
20
tests-api/mocks/utils.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
Mock utils module for testing purposes
|
||||||
|
"""
|
||||||
|
|
||||||
|
def merge_json_recursive(a, b):
|
||||||
|
"""
|
||||||
|
Mock implementation of merge_json_recursive
|
||||||
|
"""
|
||||||
|
if isinstance(a, dict) and isinstance(b, dict):
|
||||||
|
result = a.copy()
|
||||||
|
for key, value in b.items():
|
||||||
|
if key in result and isinstance(result[key], (dict, list)) and isinstance(value, (dict, list)):
|
||||||
|
result[key] = merge_json_recursive(result[key], value)
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
elif isinstance(a, list) and isinstance(b, list):
|
||||||
|
return a + b
|
||||||
|
else:
|
||||||
|
return b
|
||||||
382
tests-api/openapi.yaml
Normal file
382
tests-api/openapi.yaml
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: ComfyUI-Manager API
|
||||||
|
description: API for managing ComfyUI extensions, custom nodes, and models
|
||||||
|
version: 1.0.0
|
||||||
|
contact:
|
||||||
|
name: ComfyUI Community
|
||||||
|
url: https://github.com/comfyanonymous/ComfyUI
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:8188
|
||||||
|
description: Local ComfyUI server
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/customnode/getlist:
|
||||||
|
get:
|
||||||
|
summary: Get the list of custom nodes
|
||||||
|
description: Returns the list of custom nodes from all configured channels
|
||||||
|
parameters:
|
||||||
|
- name: mode
|
||||||
|
in: query
|
||||||
|
description: "The mode to retrieve (local=installed nodes, remote=available nodes)"
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [local, remote]
|
||||||
|
default: remote
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of custom nodes
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
nodes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CustomNode'
|
||||||
|
'500':
|
||||||
|
description: Server error
|
||||||
|
|
||||||
|
/customnode/get_node_mappings:
|
||||||
|
get:
|
||||||
|
summary: Get mappings between node class names and their custom nodes
|
||||||
|
description: Returns mappings that help identify which custom node package provides specific node classes
|
||||||
|
parameters:
|
||||||
|
- name: mode
|
||||||
|
in: query
|
||||||
|
description: "The mode for mappings (local=installed nodes, nickname=node nicknames)"
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [local, nickname]
|
||||||
|
default: local
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Node mappings
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Server error
|
||||||
|
|
||||||
|
/customnode/get_node_alternatives:
|
||||||
|
get:
|
||||||
|
summary: Get alternative nodes for specific node classes
|
||||||
|
description: Returns alternative implementations of node classes from different custom node packages
|
||||||
|
parameters:
|
||||||
|
- name: mode
|
||||||
|
in: query
|
||||||
|
description: "The mode to retrieve alternatives (local=installed nodes, remote=all available nodes)"
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [local, remote]
|
||||||
|
default: remote
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Node alternatives
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
'500':
|
||||||
|
description: Server error
|
||||||
|
|
||||||
|
/externalmodel/getlist:
|
||||||
|
get:
|
||||||
|
summary: Get the list of external models
|
||||||
|
description: Returns the list of models from all configured channels
|
||||||
|
parameters:
|
||||||
|
- name: mode
|
||||||
|
in: query
|
||||||
|
description: "The mode to retrieve (local=installed models, remote=available models)"
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [local, remote]
|
||||||
|
default: remote
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of external models
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
models:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ExternalModel'
|
||||||
|
'500':
|
||||||
|
description: Server error
|
||||||
|
|
||||||
|
/manager/get_config:
|
||||||
|
get:
|
||||||
|
summary: Get manager configuration
|
||||||
|
description: Returns the current configuration of ComfyUI-Manager
|
||||||
|
parameters:
|
||||||
|
- name: key
|
||||||
|
in: query
|
||||||
|
description: "The configuration key to retrieve"
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Configuration value
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
'400':
|
||||||
|
description: Invalid key or missing parameter
|
||||||
|
'500':
|
||||||
|
description: Server error
|
||||||
|
|
||||||
|
/manager/set_config:
|
||||||
|
post:
|
||||||
|
summary: Set manager configuration
|
||||||
|
description: Updates the configuration of ComfyUI-Manager
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- key
|
||||||
|
- value
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
description: "The configuration key to update"
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
description: "The new value for the configuration key"
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Configuration updated successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
'400':
|
||||||
|
description: Invalid key or value
|
||||||
|
'500':
|
||||||
|
description: Server error
|
||||||
|
|
||||||
|
/snapshot/getlist:
|
||||||
|
get:
|
||||||
|
summary: Get the list of snapshots
|
||||||
|
description: Returns the list of saved snapshots
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: List of snapshots
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
snapshots:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Snapshot'
|
||||||
|
'500':
|
||||||
|
description: Server error
|
||||||
|
|
||||||
|
/comfyui_manager/queue/status:
|
||||||
|
get:
|
||||||
|
summary: Get queue status
|
||||||
|
description: Returns the current status of the operation queue
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Queue status
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/QueueStatus'
|
||||||
|
'500':
|
||||||
|
description: Server error
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
CustomNode:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- title
|
||||||
|
- reference
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: "Internal name/ID of the custom node"
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description: "Display title of the custom node"
|
||||||
|
reference:
|
||||||
|
type: string
|
||||||
|
description: "Reference URL (usually GitHub repository URL)"
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: "Description of what the custom node does"
|
||||||
|
install_type:
|
||||||
|
type: string
|
||||||
|
enum: [git, pip, copy]
|
||||||
|
description: "Installation method for the custom node"
|
||||||
|
files:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "List of files provided by this custom node"
|
||||||
|
node_class_names:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "List of node class names provided by this custom node"
|
||||||
|
installed:
|
||||||
|
type: boolean
|
||||||
|
description: "Whether the custom node is installed"
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: "Version of the custom node"
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "Tags associated with the custom node"
|
||||||
|
|
||||||
|
ExternalModel:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- type
|
||||||
|
- url
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: "Name of the model"
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: "Type of the model (checkpoint, lora, embedding, etc.)"
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
description: "Download URL for the model"
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: "Description of the model"
|
||||||
|
size:
|
||||||
|
type: integer
|
||||||
|
description: "Size of the model in bytes"
|
||||||
|
installed:
|
||||||
|
type: boolean
|
||||||
|
description: "Whether the model is installed"
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: "Version of the model"
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "Tags associated with the model"
|
||||||
|
|
||||||
|
Snapshot:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- date
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: "Name of the snapshot"
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: "Date when the snapshot was created"
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: "Description of the snapshot"
|
||||||
|
nodes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "List of custom nodes in the snapshot"
|
||||||
|
models:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "List of models in the snapshot"
|
||||||
|
|
||||||
|
QueueStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pending:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/QueueItem'
|
||||||
|
description: "List of pending operations in the queue"
|
||||||
|
completed:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/QueueItem'
|
||||||
|
description: "List of completed operations in the queue"
|
||||||
|
failed:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/QueueItem'
|
||||||
|
description: "List of failed operations in the queue"
|
||||||
|
running:
|
||||||
|
type: boolean
|
||||||
|
description: "Whether the queue is currently running"
|
||||||
|
|
||||||
|
QueueItem:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- type
|
||||||
|
- target
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: "Unique ID of the queue item"
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum: [install, update, uninstall]
|
||||||
|
description: "Type of operation"
|
||||||
|
target:
|
||||||
|
type: string
|
||||||
|
description: "Target of the operation (e.g., custom node name, model name)"
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [pending, processing, completed, failed]
|
||||||
|
description: "Current status of the operation"
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: "Error message if the operation failed"
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: "Time when the operation was added to the queue"
|
||||||
|
completed_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: "Time when the operation was completed"
|
||||||
|
|
||||||
|
securitySchemes:
|
||||||
|
ApiKeyAuth:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: X-API-Key
|
||||||
|
description: "API key for authentication"
|
||||||
6
tests-api/requirements-test.txt
Normal file
6
tests-api/requirements-test.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pytest>=7.3.1
|
||||||
|
requests>=2.31.0
|
||||||
|
openapi-spec-validator>=0.6.0
|
||||||
|
jsonschema>=4.17.3
|
||||||
|
pytest-asyncio>=0.21.0
|
||||||
|
pyyaml>=6.0
|
||||||
270
tests-api/test_config_api.py
Normal file
270
tests-api/test_config_api.py
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
"""
|
||||||
|
Tests for configuration endpoints.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from typing import Callable, Dict, List, Tuple
|
||||||
|
|
||||||
|
from utils.validation import validate_response
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_preview_method(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test getting the current preview method.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/manager/preview_method"
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response is one of the valid preview methods
|
||||||
|
assert response.text in ["auto", "latent2rgb", "taesd", "none"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_db_mode(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test getting the current database mode.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/manager/db_mode"
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response is one of the valid database modes
|
||||||
|
assert response.text in ["channel", "local", "remote"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_component_policy(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test getting the current component policy.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/manager/policy/component"
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Component policy could be any string
|
||||||
|
assert response.text is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_update_policy(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test getting the current update policy.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/manager/policy/update"
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response is one of the valid update policies
|
||||||
|
assert response.text in ["stable", "nightly", "nightly-comfyui"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_url_list(
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test getting the channel URL list.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/manager/channel_url_list"
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate response structure against the schema
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path=path,
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response contains the expected fields
|
||||||
|
assert "selected" in json_data
|
||||||
|
assert "list" in json_data
|
||||||
|
assert isinstance(json_data["list"], list)
|
||||||
|
|
||||||
|
# Each channel should have a name and URL
|
||||||
|
if json_data["list"]:
|
||||||
|
first_channel = json_data["list"][0]
|
||||||
|
assert "name" in first_channel
|
||||||
|
assert "url" in first_channel
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_manager_version(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test getting the manager version.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/manager/version"
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response is a version string
|
||||||
|
assert response.text.startswith("V") # Version strings start with V
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_manager_notice(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test getting the manager notice.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/manager/notice"
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response is HTML content
|
||||||
|
assert response.headers.get("Content-Type", "").startswith("text/html") or "ComfyUI" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying operations")
|
||||||
|
class TestConfigChanges:
|
||||||
|
"""
|
||||||
|
Tests for changing configuration settings.
|
||||||
|
These are skipped to avoid modifying state in automated tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class", autouse=True)
|
||||||
|
def save_original_config(self, api_request: Callable):
|
||||||
|
"""
|
||||||
|
Save the original configuration to restore after tests.
|
||||||
|
"""
|
||||||
|
# Save original values
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/preview_method",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
self.original_preview_method = response.text
|
||||||
|
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/db_mode",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
self.original_db_mode = response.text
|
||||||
|
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/policy/update",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
self.original_update_policy = response.text
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Restore original values
|
||||||
|
api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/preview_method",
|
||||||
|
params={"value": self.original_preview_method},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/db_mode",
|
||||||
|
params={"value": self.original_db_mode},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/policy/update",
|
||||||
|
params={"value": self.original_update_policy},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_set_preview_method(self, api_request: Callable):
|
||||||
|
"""
|
||||||
|
Test setting the preview method.
|
||||||
|
"""
|
||||||
|
# Set to a different value (taesd)
|
||||||
|
api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/preview_method",
|
||||||
|
params={"value": "taesd"},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify it was changed
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/preview_method",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
assert response.text == "taesd"
|
||||||
|
|
||||||
|
def test_set_db_mode(self, api_request: Callable):
|
||||||
|
"""
|
||||||
|
Test setting the database mode.
|
||||||
|
"""
|
||||||
|
# Set to local mode
|
||||||
|
api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/db_mode",
|
||||||
|
params={"value": "local"},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify it was changed
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/db_mode",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
assert response.text == "local"
|
||||||
|
|
||||||
|
def test_set_update_policy(self, api_request: Callable):
|
||||||
|
"""
|
||||||
|
Test setting the update policy.
|
||||||
|
"""
|
||||||
|
# Set to stable
|
||||||
|
api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/policy/update",
|
||||||
|
params={"value": "stable"},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify it was changed
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/policy/update",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
assert response.text == "stable"
|
||||||
200
tests-api/test_customnode_api.py
Normal file
200
tests-api/test_customnode_api.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
"""
|
||||||
|
Tests for custom node management endpoints.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Dict, Tuple
|
||||||
|
|
||||||
|
from utils.validation import validate_response
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mode",
|
||||||
|
["local", "remote"]
|
||||||
|
)
|
||||||
|
def test_get_custom_node_list(
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict,
|
||||||
|
mode: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for listing custom nodes.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/customnode/getlist"
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
params={"mode": mode, "skip_update": "true"},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate response structure against the schema
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path=path,
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response contains the expected fields
|
||||||
|
assert "channel" in json_data
|
||||||
|
assert "node_packs" in json_data
|
||||||
|
assert isinstance(json_data["node_packs"], dict)
|
||||||
|
|
||||||
|
# If there are any node packs, verify they have the expected structure
|
||||||
|
if json_data["node_packs"]:
|
||||||
|
# Take the first node pack to validate
|
||||||
|
first_node_pack = next(iter(json_data["node_packs"].values()))
|
||||||
|
assert "title" in first_node_pack
|
||||||
|
assert "name" in first_node_pack
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_installed_nodes(
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for listing installed nodes.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/customnode/installed"
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate response structure against the schema
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path=path,
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response is a dictionary of node packs
|
||||||
|
assert isinstance(json_data, dict)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mode",
|
||||||
|
["local", "nickname"]
|
||||||
|
)
|
||||||
|
def test_get_node_mappings(
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict,
|
||||||
|
mode: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for getting node-to-package mappings.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/customnode/getmappings"
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
params={"mode": mode},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate response structure against the schema
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path=path,
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response is a dictionary mapping extension IDs to node info
|
||||||
|
assert isinstance(json_data, dict)
|
||||||
|
|
||||||
|
# If there are any mappings, verify they have the expected structure
|
||||||
|
if json_data:
|
||||||
|
# Take the first mapping to validate
|
||||||
|
first_mapping = next(iter(json_data.values()))
|
||||||
|
assert isinstance(first_mapping, list)
|
||||||
|
assert len(first_mapping) == 2
|
||||||
|
assert isinstance(first_mapping[0], list) # List of node classes
|
||||||
|
assert isinstance(first_mapping[1], dict) # Metadata
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mode",
|
||||||
|
["local", "remote"]
|
||||||
|
)
|
||||||
|
def test_get_node_alternatives(
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict,
|
||||||
|
mode: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for getting alternative node options.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/customnode/alternatives"
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
params={"mode": mode},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate response structure against the schema
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path=path,
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response is a dictionary
|
||||||
|
assert isinstance(json_data, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_updates(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for fetching updates.
|
||||||
|
This might modify state, so we just check for a valid response.
|
||||||
|
"""
|
||||||
|
# Make the API request with skip_update=true to avoid actual updates
|
||||||
|
path = "/customnode/fetch_updates"
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
params={"mode": "local"},
|
||||||
|
# Don't validate JSON since this endpoint doesn't return JSON
|
||||||
|
expected_status=200,
|
||||||
|
retry_on_error=False, # Don't retry as this might have side effects
|
||||||
|
)
|
||||||
|
|
||||||
|
# Just check the status code is as expected (covered by api_request)
|
||||||
|
assert response.status_code in [200, 201]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Queue endpoints are better tested with queue operations")
|
||||||
|
def test_queue_update_all(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for queuing updates for all nodes.
|
||||||
|
Skipping as this would actually modify the installation.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Security-restricted endpoint")
|
||||||
|
def test_install_node_via_git_url(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for installing a node via Git URL.
|
||||||
|
Skipping as this requires high security level and would modify the installation.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
23
tests-api/test_import.py
Normal file
23
tests-api/test_import.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Print current working directory
|
||||||
|
print(f"Current directory: {os.getcwd()}")
|
||||||
|
|
||||||
|
# Print module search path
|
||||||
|
print(f"System path: {sys.path}")
|
||||||
|
|
||||||
|
# Try to import
|
||||||
|
try:
|
||||||
|
from utils.validation import load_openapi_spec
|
||||||
|
print("Import successful!")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Import error: {e}")
|
||||||
|
|
||||||
|
# Try direct import
|
||||||
|
try:
|
||||||
|
sys.path.insert(0, os.path.join(os.getcwd(), "custom_nodes/ComfyUI-Manager/tests-api"))
|
||||||
|
from utils.validation import load_openapi_spec
|
||||||
|
print("Direct import successful!")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Direct import error: {e}")
|
||||||
62
tests-api/test_model_api.py
Normal file
62
tests-api/test_model_api.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
Tests for model management endpoints.
|
||||||
|
These features are scheduled for deprecation, so tests are minimal.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from typing import Callable, Dict
|
||||||
|
|
||||||
|
from utils.validation import validate_response
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mode",
|
||||||
|
["local", "remote"]
|
||||||
|
)
|
||||||
|
def test_get_external_model_list(
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict,
|
||||||
|
mode: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for listing external models.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/externalmodel/getlist"
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
params={"mode": mode},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate response structure against the schema
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path=path,
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response contains the expected fields
|
||||||
|
assert "models" in json_data
|
||||||
|
assert isinstance(json_data["models"], list)
|
||||||
|
|
||||||
|
# If there are any models, verify they have the expected structure
|
||||||
|
if json_data["models"]:
|
||||||
|
first_model = json_data["models"][0]
|
||||||
|
assert "name" in first_model
|
||||||
|
assert "type" in first_model
|
||||||
|
assert "url" in first_model
|
||||||
|
assert "filename" in first_model
|
||||||
|
assert "installed" in first_model
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying operation that requires auth")
|
||||||
|
def test_install_model():
|
||||||
|
"""
|
||||||
|
Test queuing a model installation.
|
||||||
|
Skipped to avoid modifying state and requires authentication.
|
||||||
|
This feature is also scheduled for deprecation.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
213
tests-api/test_queue_api.py
Normal file
213
tests-api/test_queue_api.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
"""
|
||||||
|
Tests for queue management endpoints.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Dict, Tuple
|
||||||
|
|
||||||
|
from utils.validation import validate_response
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_queue_status(
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for getting queue status.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/manager/queue/status"
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate response structure against the schema
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path=path,
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response contains the expected fields
|
||||||
|
assert "total_count" in json_data
|
||||||
|
assert "done_count" in json_data
|
||||||
|
assert "in_progress_count" in json_data
|
||||||
|
assert "is_processing" in json_data
|
||||||
|
|
||||||
|
# Type checks
|
||||||
|
assert isinstance(json_data["total_count"], int)
|
||||||
|
assert isinstance(json_data["done_count"], int)
|
||||||
|
assert isinstance(json_data["in_progress_count"], int)
|
||||||
|
assert isinstance(json_data["is_processing"], bool)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_queue(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for resetting the queue.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/manager/queue/reset"
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now check the queue status to verify it was reset
|
||||||
|
response2, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/queue/status",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Queue should be empty after reset
|
||||||
|
assert json_data["total_count"] == json_data["done_count"] + json_data["in_progress_count"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying operation that requires auth")
|
||||||
|
def test_queue_install_node():
|
||||||
|
"""
|
||||||
|
Test queuing a node installation.
|
||||||
|
Skipped to avoid modifying state and requires authentication.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying operation that requires auth")
|
||||||
|
def test_queue_update_node():
|
||||||
|
"""
|
||||||
|
Test queuing a node update.
|
||||||
|
Skipped to avoid modifying state and requires authentication.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying operation that requires auth")
|
||||||
|
def test_queue_uninstall_node():
|
||||||
|
"""
|
||||||
|
Test queuing a node uninstallation.
|
||||||
|
Skipped to avoid modifying state and requires authentication.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying operation")
|
||||||
|
def test_queue_start():
|
||||||
|
"""
|
||||||
|
Test starting the queue.
|
||||||
|
Skipped to avoid modifying state.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestQueueOperations:
|
||||||
|
"""
|
||||||
|
Test a complete queue workflow.
|
||||||
|
These tests are grouped to ensure proper sequencing but are still skipped
|
||||||
|
to avoid modifying state in automated tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def node_data(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Create test data for a node operation.
|
||||||
|
"""
|
||||||
|
# This would be replaced with actual data for a known safe node
|
||||||
|
return {
|
||||||
|
"ui_id": "test_node_1",
|
||||||
|
"id": "comfyui-manager", # Manager itself
|
||||||
|
"version": "latest",
|
||||||
|
"channel": "default",
|
||||||
|
"mode": "local",
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying operation")
|
||||||
|
def test_queue_operation_sequence(
|
||||||
|
self,
|
||||||
|
api_request: Callable,
|
||||||
|
node_data: Dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the queue operation sequence.
|
||||||
|
"""
|
||||||
|
# 1. Reset the queue
|
||||||
|
api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/queue/reset",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Queue a node operation (we'll use the manager itself)
|
||||||
|
api_request(
|
||||||
|
method="post",
|
||||||
|
path="/manager/queue/update",
|
||||||
|
json_data=node_data,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Check queue status - should have one operation
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/queue/status",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json_data["total_count"] > 0
|
||||||
|
assert not json_data["is_processing"] # Queue hasn't started yet
|
||||||
|
|
||||||
|
# 4. Start the queue
|
||||||
|
api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/queue/start",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Check queue status again - should be processing
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/queue/status",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Queue should be processing or already done
|
||||||
|
assert json_data["is_processing"] or json_data["done_count"] == json_data["total_count"]
|
||||||
|
|
||||||
|
# 6. Wait for queue to complete (with timeout)
|
||||||
|
max_wait_time = 60 # seconds
|
||||||
|
start_time = time.time()
|
||||||
|
completed = False
|
||||||
|
|
||||||
|
while time.time() - start_time < max_wait_time:
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/manager/queue/status",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
if json_data["done_count"] == json_data["total_count"] and not json_data["is_processing"]:
|
||||||
|
completed = True
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(2) # Wait before checking again
|
||||||
|
|
||||||
|
assert completed, "Queue did not complete within timeout period"
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying operation")
|
||||||
|
def test_concurrent_queue_operations(
|
||||||
|
self,
|
||||||
|
api_request: Callable,
|
||||||
|
node_data: Dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test concurrent queue operations.
|
||||||
|
"""
|
||||||
|
# This would test adding multiple operations to the queue
|
||||||
|
# and verifying they all complete correctly
|
||||||
|
pass
|
||||||
198
tests-api/test_snapshot_api.py
Normal file
198
tests-api/test_snapshot_api.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
"""
|
||||||
|
Tests for snapshot management endpoints.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Dict, List, Optional
|
||||||
|
|
||||||
|
from utils.validation import validate_response
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_snapshot_list(
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for listing snapshots.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/snapshot/getlist"
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate response structure against the schema
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path=path,
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the response contains the expected fields
|
||||||
|
assert "items" in json_data
|
||||||
|
assert isinstance(json_data["items"], list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_current_snapshot(
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for getting the current snapshot.
|
||||||
|
"""
|
||||||
|
# Make the API request
|
||||||
|
path = "/snapshot/get_current"
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path=path,
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate response structure against the schema
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path=path,
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for basic snapshot structure
|
||||||
|
assert "snapshot_date" in json_data
|
||||||
|
assert "custom_nodes" in json_data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="This test creates a snapshot which is a state-modifying operation")
|
||||||
|
def test_save_snapshot(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for saving a new snapshot.
|
||||||
|
Skipped to avoid modifying state in tests.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="This test removes a snapshot which is a destructive operation")
|
||||||
|
def test_remove_snapshot(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for removing a snapshot.
|
||||||
|
Skipped to avoid modifying state in tests.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="This test restores a snapshot which is a state-modifying operation")
|
||||||
|
def test_restore_snapshot(
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test the endpoint for restoring a snapshot.
|
||||||
|
Skipped to avoid modifying state in tests.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestSnapshotWorkflow:
|
||||||
|
"""
|
||||||
|
Test the complete snapshot workflow (create, list, get, remove).
|
||||||
|
These tests are grouped to ensure proper sequencing but are still skipped
|
||||||
|
to avoid modifying state in automated tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def snapshot_name(self) -> str:
|
||||||
|
"""
|
||||||
|
Generate a unique snapshot name for testing.
|
||||||
|
"""
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
return f"test_snapshot_{timestamp}"
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying test")
|
||||||
|
def test_create_snapshot(
|
||||||
|
self,
|
||||||
|
api_request: Callable,
|
||||||
|
snapshot_name: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test creating a snapshot.
|
||||||
|
"""
|
||||||
|
# Make the API request to save a snapshot
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/snapshot/save",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify a snapshot was created (would need to check the snapshot list)
|
||||||
|
response2, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/snapshot/getlist",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The most recently created snapshot should be first in the list
|
||||||
|
assert json_data["items"]
|
||||||
|
|
||||||
|
# Store the snapshot name for later tests
|
||||||
|
self.actual_snapshot_name = json_data["items"][0]
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying test")
|
||||||
|
def test_get_snapshot_details(
|
||||||
|
self,
|
||||||
|
api_request: Callable,
|
||||||
|
openapi_spec: Dict
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test getting details of the created snapshot.
|
||||||
|
"""
|
||||||
|
# This would check the current snapshot, not a specific one
|
||||||
|
# since there's no direct API to get a specific snapshot
|
||||||
|
response, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/snapshot/get_current",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate the snapshot data
|
||||||
|
assert json_data is not None
|
||||||
|
validate_response(
|
||||||
|
response_data=json_data,
|
||||||
|
path="/snapshot/get_current",
|
||||||
|
method="get",
|
||||||
|
spec=openapi_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="State-modifying test")
|
||||||
|
def test_remove_test_snapshot(
|
||||||
|
self,
|
||||||
|
api_request: Callable
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test removing the test snapshot.
|
||||||
|
"""
|
||||||
|
# Make the API request to remove the snapshot
|
||||||
|
response, _ = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/snapshot/remove",
|
||||||
|
params={"target": self.actual_snapshot_name},
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the snapshot was removed
|
||||||
|
response2, json_data = api_request(
|
||||||
|
method="get",
|
||||||
|
path="/snapshot/getlist",
|
||||||
|
expected_status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The snapshot should no longer be in the list
|
||||||
|
assert self.actual_snapshot_name not in json_data["items"]
|
||||||
150
tests-api/test_spec_validation.py
Normal file
150
tests-api/test_spec_validation.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""
|
||||||
|
Tests for validating the OpenAPI specification.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
from typing import Dict, Any, List, Tuple
|
||||||
|
from pathlib import Path
|
||||||
|
from openapi_spec_validator import validate_spec
|
||||||
|
from utils.validation import load_openapi_spec
|
||||||
|
from utils.schema_utils import (
|
||||||
|
get_all_paths,
|
||||||
|
get_methods_for_path,
|
||||||
|
find_paths_with_security,
|
||||||
|
get_required_parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_spec_is_valid():
|
||||||
|
"""
|
||||||
|
Test that the OpenAPI specification is valid according to the spec validator.
|
||||||
|
"""
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
validate_spec(spec)
|
||||||
|
|
||||||
|
|
||||||
|
def test_spec_has_info():
|
||||||
|
"""
|
||||||
|
Test that the OpenAPI specification has basic info.
|
||||||
|
"""
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
|
||||||
|
assert "info" in spec
|
||||||
|
assert "title" in spec["info"]
|
||||||
|
assert "version" in spec["info"]
|
||||||
|
assert spec["info"]["title"] == "ComfyUI-Manager API"
|
||||||
|
|
||||||
|
|
||||||
|
def test_spec_has_paths():
|
||||||
|
"""
|
||||||
|
Test that the OpenAPI specification has paths defined.
|
||||||
|
"""
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
|
||||||
|
assert "paths" in spec
|
||||||
|
assert len(spec["paths"]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_paths_have_responses():
|
||||||
|
"""
|
||||||
|
Test that all paths have responses defined.
|
||||||
|
"""
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
|
||||||
|
for path, path_item in spec["paths"].items():
|
||||||
|
for method, operation in path_item.items():
|
||||||
|
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
assert "responses" in operation, f"Path {path} method {method} has no responses"
|
||||||
|
assert len(operation["responses"]) > 0, f"Path {path} method {method} has empty responses"
|
||||||
|
|
||||||
|
|
||||||
|
def test_responses_have_schemas():
|
||||||
|
"""
|
||||||
|
Test that responses with application/json content type have schemas.
|
||||||
|
"""
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
|
||||||
|
for path, path_item in spec["paths"].items():
|
||||||
|
for method, operation in path_item.items():
|
||||||
|
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for status, response in operation["responses"].items():
|
||||||
|
if "content" not in response:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "application/json" in response["content"]:
|
||||||
|
assert "schema" in response["content"]["application/json"], (
|
||||||
|
f"Path {path} method {method} status {status} "
|
||||||
|
f"application/json content has no schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_required_parameters_have_schemas():
|
||||||
|
"""
|
||||||
|
Test that all required parameters have schemas.
|
||||||
|
"""
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
|
||||||
|
for path, path_item in spec["paths"].items():
|
||||||
|
for method, operation in path_item.items():
|
||||||
|
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "parameters" not in operation:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for param in operation["parameters"]:
|
||||||
|
if param.get("required", False):
|
||||||
|
assert "schema" in param, (
|
||||||
|
f"Path {path} method {method} required parameter {param.get('name')} has no schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_security_schemes_defined():
|
||||||
|
"""
|
||||||
|
Test that security schemes are properly defined.
|
||||||
|
"""
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
|
||||||
|
# Get paths requiring security
|
||||||
|
secure_paths = find_paths_with_security(spec)
|
||||||
|
|
||||||
|
if secure_paths:
|
||||||
|
assert "components" in spec, "Spec has secure paths but no components"
|
||||||
|
assert "securitySchemes" in spec["components"], "Spec has secure paths but no securitySchemes"
|
||||||
|
|
||||||
|
# Check each security reference is defined
|
||||||
|
for path, method in secure_paths:
|
||||||
|
operation = spec["paths"][path][method]
|
||||||
|
for security_req in operation["security"]:
|
||||||
|
for scheme_name in security_req:
|
||||||
|
assert scheme_name in spec["components"]["securitySchemes"], (
|
||||||
|
f"Security scheme {scheme_name} used by {method.upper()} {path} "
|
||||||
|
f"is not defined in components.securitySchemes"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_common_endpoint_groups_present():
|
||||||
|
"""
|
||||||
|
Test that the spec includes the main endpoint groups.
|
||||||
|
"""
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
paths = get_all_paths(spec)
|
||||||
|
|
||||||
|
# Define the expected endpoint prefixes
|
||||||
|
expected_prefixes = [
|
||||||
|
"/customnode/",
|
||||||
|
"/externalmodel/",
|
||||||
|
"/manager/",
|
||||||
|
"/snapshot/",
|
||||||
|
"/comfyui_manager/",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check that at least one path exists for each expected prefix
|
||||||
|
for prefix in expected_prefixes:
|
||||||
|
matching_paths = [p for p in paths if p.startswith(prefix)]
|
||||||
|
assert matching_paths, f"No endpoints found with prefix {prefix}"
|
||||||
1
tests-api/utils/__init__.py
Normal file
1
tests-api/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Make utils directory a proper package
|
||||||
174
tests-api/utils/schema_utils.py
Normal file
174
tests-api/utils/schema_utils.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
Schema utilities for extracting and manipulating OpenAPI schemas.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||||
|
from .validation import load_openapi_spec
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_paths(spec: Dict[str, Any]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get all paths defined in the OpenAPI specification.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: The OpenAPI specification
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all paths
|
||||||
|
"""
|
||||||
|
return list(spec.get("paths", {}).keys())
|
||||||
|
|
||||||
|
|
||||||
|
def get_grouped_paths(spec: Dict[str, Any]) -> Dict[str, List[str]]:
|
||||||
|
"""
|
||||||
|
Group paths by their top-level segment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: The OpenAPI specification
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping top-level segments to lists of paths
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for path in get_all_paths(spec):
|
||||||
|
segments = path.strip("/").split("/")
|
||||||
|
if not segments:
|
||||||
|
continue
|
||||||
|
|
||||||
|
top_segment = segments[0]
|
||||||
|
if top_segment not in result:
|
||||||
|
result[top_segment] = []
|
||||||
|
|
||||||
|
result[top_segment].append(path)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_methods_for_path(spec: Dict[str, Any], path: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get all HTTP methods defined for a path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: The OpenAPI specification
|
||||||
|
path: The API path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of HTTP methods (lowercase)
|
||||||
|
"""
|
||||||
|
if path not in spec.get("paths", {}):
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
method.lower()
|
||||||
|
for method in spec["paths"][path].keys()
|
||||||
|
if method.lower() in {"get", "post", "put", "delete", "patch", "options", "head"}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def find_paths_with_security(
|
||||||
|
spec: Dict[str, Any],
|
||||||
|
security_scheme: Optional[str] = None
|
||||||
|
) -> List[Tuple[str, str]]:
|
||||||
|
"""
|
||||||
|
Find all paths that require security.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: The OpenAPI specification
|
||||||
|
security_scheme: Optional specific security scheme to filter by
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of (path, method) tuples that require security
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for path, path_item in spec.get("paths", {}).items():
|
||||||
|
for method, operation in path_item.items():
|
||||||
|
if method.lower() not in {"get", "post", "put", "delete", "patch", "options", "head"}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "security" in operation:
|
||||||
|
if security_scheme is None:
|
||||||
|
result.append((path, method.lower()))
|
||||||
|
else:
|
||||||
|
# Check if this security scheme is required
|
||||||
|
for security_req in operation["security"]:
|
||||||
|
if security_scheme in security_req:
|
||||||
|
result.append((path, method.lower()))
|
||||||
|
break
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_content_types_for_response(
|
||||||
|
spec: Dict[str, Any],
|
||||||
|
path: str,
|
||||||
|
method: str,
|
||||||
|
status_code: str = "200"
|
||||||
|
) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get content types defined for a response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: The OpenAPI specification
|
||||||
|
path: The API path
|
||||||
|
method: The HTTP method
|
||||||
|
status_code: The HTTP status code
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of content types
|
||||||
|
"""
|
||||||
|
method = method.lower()
|
||||||
|
|
||||||
|
if path not in spec["paths"]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if method not in spec["paths"][path]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if "responses" not in spec["paths"][path][method]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if status_code not in spec["paths"][path][method]["responses"]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
response_def = spec["paths"][path][method]["responses"][status_code]
|
||||||
|
|
||||||
|
if "content" not in response_def:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return list(response_def["content"].keys())
|
||||||
|
|
||||||
|
|
||||||
|
def get_required_parameters(
|
||||||
|
spec: Dict[str, Any],
|
||||||
|
path: str,
|
||||||
|
method: str
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get all required parameters for a path/method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: The OpenAPI specification
|
||||||
|
path: The API path
|
||||||
|
method: The HTTP method
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of parameter objects that are required
|
||||||
|
"""
|
||||||
|
method = method.lower()
|
||||||
|
|
||||||
|
if path not in spec["paths"]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if method not in spec["paths"][path]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if "parameters" not in spec["paths"][path][method]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
param for param in spec["paths"][path][method]["parameters"]
|
||||||
|
if param.get("required", False)
|
||||||
|
]
|
||||||
155
tests-api/utils/validation.py
Normal file
155
tests-api/utils/validation.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
"""
|
||||||
|
Validation utilities for API tests.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import jsonschema
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
def load_openapi_spec(spec_path: Union[str, Path] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Load the OpenAPI specification document.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec_path: Path to the OpenAPI specification file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The OpenAPI specification as a dictionary
|
||||||
|
"""
|
||||||
|
if spec_path is None:
|
||||||
|
# Default to the root openapi.yaml file
|
||||||
|
spec_path = Path(__file__).parents[2] / "openapi.yaml"
|
||||||
|
|
||||||
|
with open(spec_path, "r") as f:
|
||||||
|
if str(spec_path).endswith(".yaml") or str(spec_path).endswith(".yml"):
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
else:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def get_schema_for_path(
|
||||||
|
spec: Dict[str, Any],
|
||||||
|
path: str,
|
||||||
|
method: str,
|
||||||
|
status_code: str = "200",
|
||||||
|
content_type: str = "application/json"
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Extract the response schema for a specific path, method, and status code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec: The OpenAPI specification
|
||||||
|
path: The API path (e.g., "/customnode/getlist")
|
||||||
|
method: The HTTP method (e.g., "get", "post")
|
||||||
|
status_code: The HTTP status code (default: "200")
|
||||||
|
content_type: The response content type (default: "application/json")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The schema for the specified path and method, or None if not found
|
||||||
|
"""
|
||||||
|
method = method.lower()
|
||||||
|
|
||||||
|
if path not in spec["paths"]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if method not in spec["paths"][path]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "responses" not in spec["paths"][path][method]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if status_code not in spec["paths"][path][method]["responses"]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
response_def = spec["paths"][path][method]["responses"][status_code]
|
||||||
|
|
||||||
|
if "content" not in response_def:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if content_type not in response_def["content"]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "schema" not in response_def["content"][content_type]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return response_def["content"][content_type]["schema"]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_response_schema(
|
||||||
|
response_data: Any,
|
||||||
|
schema: Dict[str, Any],
|
||||||
|
spec: Dict[str, Any] = None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Validate a response against a schema from the OpenAPI specification.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response_data: The response data to validate
|
||||||
|
schema: The schema to validate against
|
||||||
|
spec: The complete OpenAPI specification (for resolving references)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if validation succeeds, raises an exception otherwise
|
||||||
|
"""
|
||||||
|
if spec is None:
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
|
||||||
|
# Create a resolver for references within the schema
|
||||||
|
resolver = jsonschema.RefResolver.from_schema(spec)
|
||||||
|
|
||||||
|
# Validate the response against the schema
|
||||||
|
jsonschema.validate(
|
||||||
|
instance=response_data,
|
||||||
|
schema=schema,
|
||||||
|
resolver=resolver
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate_response(
|
||||||
|
response_data: Any,
|
||||||
|
path: str,
|
||||||
|
method: str,
|
||||||
|
status_code: str = "200",
|
||||||
|
content_type: str = "application/json",
|
||||||
|
spec: Dict[str, Any] = None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Validate a response against the schema defined in the OpenAPI specification.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response_data: The response data to validate
|
||||||
|
path: The API path
|
||||||
|
method: The HTTP method
|
||||||
|
status_code: The HTTP status code (default: "200")
|
||||||
|
content_type: The response content type (default: "application/json")
|
||||||
|
spec: The OpenAPI specification (loaded from default location if None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if validation succeeds, raises an exception otherwise
|
||||||
|
"""
|
||||||
|
if spec is None:
|
||||||
|
spec = load_openapi_spec()
|
||||||
|
|
||||||
|
schema = get_schema_for_path(
|
||||||
|
spec=spec,
|
||||||
|
path=path,
|
||||||
|
method=method,
|
||||||
|
status_code=status_code,
|
||||||
|
content_type=content_type
|
||||||
|
)
|
||||||
|
|
||||||
|
if schema is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"No schema found for {method.upper()} {path} "
|
||||||
|
f"with status {status_code} and content type {content_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return validate_response_schema(
|
||||||
|
response_data=response_data,
|
||||||
|
schema=schema,
|
||||||
|
spec=spec
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user