mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-07-23 00:07:38 +08:00
Compare commits
757
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f85a12f2a2 | ||
|
|
29216e96bd | ||
|
|
3f0fc85b95 | ||
|
|
b9def4cb6e | ||
|
|
a7eb93fff0 | ||
|
|
a542695e9c | ||
|
|
2779c66b39 | ||
|
|
952613c07b | ||
|
|
75f27d99e2 | ||
|
|
8e8b6ca724 | ||
|
|
3425fb7a14 | ||
|
|
c69e7bcf03 | ||
|
|
85ebcd9897 | ||
|
|
69b6f1a66b | ||
|
|
e4a90089ab | ||
|
|
674b9f3705 | ||
|
|
4941fb8aa0 | ||
|
|
183af0dfa5 | ||
|
|
45ac5429f8 | ||
|
|
c771977a95 | ||
|
|
668d7bbb2c | ||
|
|
926cfabb58 | ||
|
|
a9a8d05115 | ||
|
|
e368f4366a | ||
|
|
dc5bddbc17 | ||
|
|
358a480408 | ||
|
|
c96fdb3c7a | ||
|
|
c090abcc02 | ||
|
|
1ff02be35f | ||
|
|
10fbfb88f7 | ||
|
|
9753df72ed | ||
|
|
095cc3f792 | ||
|
|
656171037b | ||
|
|
7ac10f9442 | ||
|
|
3925ba27b4 | ||
|
|
44ba79aa31 | ||
|
|
14d0e31268 | ||
|
|
033acffad1 | ||
|
|
d29ff808a5 | ||
|
|
dc9b6d655b | ||
|
|
d340c85013 | ||
|
|
e328353664 | ||
|
|
02785af8fd | ||
|
|
736ae5d63e | ||
|
|
e1eeb617d2 | ||
|
|
23b6c7f0de | ||
|
|
997f97e1fc | ||
|
|
ff335ff1a0 | ||
|
|
cb3036ef81 | ||
|
|
f762906188 | ||
|
|
dde7920f8c | ||
|
|
1a0d24110a | ||
|
|
e79f6c4471 | ||
|
|
a8a7024a84 | ||
|
|
d93d002da0 | ||
|
|
baaa0479e8 | ||
|
|
cc3bd7a056 | ||
|
|
4ecefb3b71 | ||
|
|
f24b5aa251 | ||
|
|
de547da4cd | ||
|
|
0f884166a6 | ||
|
|
63379f759d | ||
|
|
8fdff20243 | ||
|
|
5dfa07ca03 | ||
|
|
343645be6a | ||
|
|
91bf21d7a8 | ||
|
|
be6516cfd3 | ||
|
|
61f1e516a3 | ||
|
|
73b2278b45 | ||
|
|
aa625e30b6 | ||
|
|
29a46fe4ce | ||
|
|
5b3ee49530 | ||
|
|
a9158a101f | ||
|
|
feed8abb34 | ||
|
|
70decc740f | ||
|
|
5b5c83f8c5 | ||
|
|
773c06f40d | ||
|
|
737e6ad5ed | ||
|
|
81bca9c94e | ||
|
|
eef0654de2 | ||
|
|
997a00b8a2 | ||
|
|
4d25232c5f | ||
|
|
135befa101 | ||
|
|
44cac3fc43 | ||
|
|
449fa3510e | ||
|
|
d958af8aad | ||
|
|
09f8d5cb2d | ||
|
|
aedc99cefd | ||
|
|
b32cab6e9a | ||
|
|
a95186965e | ||
|
|
7067de1bb2 | ||
|
|
f45d878d21 | ||
|
|
a0532b938d | ||
|
|
6ad12b7652 | ||
|
|
02887c6c9b | ||
|
|
1b645e1cc3 | ||
|
|
0a4b2a0488 | ||
|
|
d4ce6ddc52 | ||
|
|
5a5b989533 | ||
|
|
b57cffb0fa | ||
|
|
72aa95cacf | ||
|
|
14ef448937 | ||
|
|
1c10607c06 | ||
|
|
41e53a1f2a | ||
|
|
cde83e9a38 | ||
|
|
6c206b1c72 | ||
|
|
a474219e7b | ||
|
|
3b8d25eb31 | ||
|
|
0faa0aa668 | ||
|
|
0fcdcc93a2 | ||
|
|
461d5e72fe | ||
|
|
3f030a2121 | ||
|
|
7fb8e8662f | ||
|
|
dd3ab9cff2 | ||
|
|
97b518de12 | ||
|
|
d2a795c866 | ||
|
|
8a3d65be20 | ||
|
|
b2126d8ba5 | ||
|
|
6386411d21 | ||
|
|
4250244136 | ||
|
|
77c4f9993d | ||
|
|
c7c8417577 | ||
|
|
9d0985ded8 | ||
|
|
3663e10e33 | ||
|
|
5f37a82c3c | ||
|
|
026bf1dfd7 | ||
|
|
643a6e5080 | ||
|
|
5267502896 | ||
|
|
c3c152122d | ||
|
|
afeac097e5 | ||
|
|
e5cea64132 | ||
|
|
26da78cf15 | ||
|
|
179a1e1ca0 | ||
|
|
b379d275d1 | ||
|
|
133cdfb203 | ||
|
|
2b79edd9be | ||
|
|
3862a92e04 | ||
|
|
f4e3817fcc | ||
|
|
61f0f5d67c | ||
|
|
87f57551ea | ||
|
|
ee51efed69 | ||
|
|
5dab865681 | ||
|
|
8c0581eebc | ||
|
|
a72f9f422c | ||
|
|
1354a8c970 | ||
|
|
00a5115267 | ||
|
|
00282eab7b | ||
|
|
bec128de58 | ||
|
|
9edfa7b4fa | ||
|
|
a9af70e5f0 | ||
|
|
910caf593f | ||
|
|
02dc072dc7 | ||
|
|
78fb354452 | ||
|
|
66f5eca7fa | ||
|
|
d3906e3cbc | ||
|
|
079ac254ce | ||
|
|
be95396a57 | ||
|
|
59cbed429f | ||
|
|
d49df7aebb | ||
|
|
0aa9faad2e | ||
|
|
1337def888 | ||
|
|
4b100c558b | ||
|
|
1425a71ece | ||
|
|
e0640e7014 | ||
|
|
a8524508fe | ||
|
|
a5ff973d53 | ||
|
|
337c9aa2c7 | ||
|
|
f1448403ac | ||
|
|
d0b5f77ec6 | ||
|
|
9cb22ffb60 | ||
|
|
f556962d82 | ||
|
|
d28448d519 | ||
|
|
c590a88ffd | ||
|
|
a1fc6c817b | ||
|
|
5554e52799 | ||
|
|
ca749eb4d2 | ||
|
|
41ceee3d24 | ||
|
|
5acfd52986 | ||
|
|
ec4c7b2f6a | ||
|
|
22a3d8f95f | ||
|
|
06b89ca277 | ||
|
|
9e5ffbd00a | ||
|
|
39e92ed778 | ||
|
|
68a3ec567a | ||
|
|
28231e81b3 | ||
|
|
b2ee0feeaa | ||
|
|
5541b6b366 | ||
|
|
408a5fe27e | ||
|
|
bffc73f976 | ||
|
|
bd6edfc9dd | ||
|
|
2cb24e8a94 | ||
|
|
a49779c4d2 | ||
|
|
15a5a5f5df | ||
|
|
b5e0558d6e | ||
|
|
4d683b23fc | ||
|
|
c13da606b2 | ||
|
|
c792f9277c | ||
|
|
b430f42622 | ||
|
|
fee822f5ae | ||
|
|
192659ecbd | ||
|
|
810431b9e2 | ||
|
|
02d845adf3 | ||
|
|
89c7b960fb | ||
|
|
ed1e399a56 | ||
|
|
8a3ce1ae57 | ||
|
|
d89ff649f8 | ||
|
|
24a73b5d1c | ||
|
|
4d0c40ff8a | ||
|
|
b5a2bed539 | ||
|
|
0efb79f571 | ||
|
|
df944b9a0f | ||
|
|
2c11846430 | ||
|
|
0035c01186 | ||
|
|
34be3384fe | ||
|
|
ebbc7b3335 | ||
|
|
4ccc8c3086 | ||
|
|
af9ebc9568 | ||
|
|
ca4b61c5f0 | ||
|
|
393839b3ab | ||
|
|
dadfc96e00 | ||
|
|
a0a33aef03 | ||
|
|
99ed81e0f5 | ||
|
|
5b697db219 | ||
|
|
8e5bf46e14 | ||
|
|
9f649b0900 | ||
|
|
abb15e06d3 | ||
|
|
11a317493e | ||
|
|
e8cece0c1b | ||
|
|
1ab882f81d | ||
|
|
b9338186e3 | ||
|
|
7c3cbff425 | ||
|
|
1ff0afc633 | ||
|
|
bfe7ee8fba | ||
|
|
49c73ed10e | ||
|
|
f571baacf9 | ||
|
|
6f02e1114c | ||
|
|
e230f43565 | ||
|
|
0d9593e71b | ||
|
|
20778ecfb0 | ||
|
|
2ea991d960 | ||
|
|
119c107834 | ||
|
|
800a0d0449 | ||
|
|
95c43f0189 | ||
|
|
9c77176c7f | ||
|
|
ddb6a55cd6 | ||
|
|
56a4f6fdd7 | ||
|
|
8a30f788b5 | ||
|
|
380a1c2c8c | ||
|
|
cd8e8335cf | ||
|
|
6e1beb54a4 | ||
|
|
9217c965dd | ||
|
|
a4d71ef487 | ||
|
|
518f332047 | ||
|
|
9257d497b8 | ||
|
|
07cf5de4f7 | ||
|
|
43ad69e48d | ||
|
|
c62e236cc6 | ||
|
|
15a2fbb293 | ||
|
|
16800c3fa0 | ||
|
|
ce09f41aa3 | ||
|
|
47dc2f036a | ||
|
|
f27a154bfd | ||
|
|
79757366e8 | ||
|
|
2cd9a417d6 | ||
|
|
deb05c6cc3 | ||
|
|
b6f171de51 | ||
|
|
a58d5f6999 | ||
|
|
e0b3f3eb45 | ||
|
|
4bbc8594a7 | ||
|
|
1ab2b1aeb3 | ||
|
|
3a377300e1 | ||
|
|
33a07e3a86 | ||
|
|
212cafc1d7 | ||
|
|
2643b3cbcc | ||
|
|
d445229b6d | ||
|
|
dab5c451b0 | ||
|
|
7bdf06131a | ||
|
|
854648d5af | ||
|
|
c5f7b97359 | ||
|
|
dd8a727ad6 | ||
|
|
6c627fe422 | ||
|
|
ee980e1caf | ||
|
|
22bfaf6527 | ||
|
|
48ab48cc30 | ||
|
|
a0b14d4127 | ||
|
|
03f9fe1a70 | ||
|
|
8915b8d796 | ||
|
|
c77ffeeec0 | ||
|
|
4acf5660b2 | ||
|
|
2d9f0a668c | ||
|
|
9e6cb246cc | ||
|
|
14544ca63d | ||
|
|
26b347c04c | ||
|
|
36f75d1811 | ||
|
|
ffaeb6d3ff | ||
|
|
6cc1ad4cc0 | ||
|
|
27fc787294 | ||
|
|
d23286d390 | ||
|
|
7c3ccc76c3 | ||
|
|
892dc5d4f3 | ||
|
|
e278692749 | ||
|
|
8d77dd2246 | ||
|
|
14ede2a585 | ||
|
|
5b525622f1 | ||
|
|
a24b11905c | ||
|
|
5d70858341 | ||
|
|
3daa006741 | ||
|
|
0bcc0c2101 | ||
|
|
b8850c808c | ||
|
|
f4f2c01ac1 | ||
|
|
7072e82dff | ||
|
|
53dc36c4cf | ||
|
|
5aadc3af00 | ||
|
|
8c28a698ed | ||
|
|
5ed6d8b202 | ||
|
|
b73dc7bf5e | ||
|
|
d7799964de | ||
|
|
71d0f4ab63 | ||
|
|
d479dcde81 | ||
|
|
ae536017d5 | ||
|
|
67ddfce279 | ||
|
|
b1f39b34d7 | ||
|
|
6cf958ccce | ||
|
|
5378f0a8e9 | ||
|
|
e13bf68775 | ||
|
|
eaed3677d3 | ||
|
|
b9c88da54d | ||
|
|
104ae77f7a | ||
|
|
bfcb2ce61b | ||
|
|
d970fe68ea | ||
|
|
63ba5fed09 | ||
|
|
98a8464933 | ||
|
|
7e3e6726e0 | ||
|
|
09567b2bb2 | ||
|
|
f3bd116184 | ||
|
|
7509737563 | ||
|
|
cfb815d879 | ||
|
|
44241fb967 | ||
|
|
c4b45129bd | ||
|
|
70741008ca | ||
|
|
6c2d2cae2a | ||
|
|
28f13d3311 | ||
|
|
4e31aaa8fb | ||
|
|
ba99f0c2cc | ||
|
|
e0a96b4937 | ||
|
|
82c055f527 | ||
|
|
f94008192c | ||
|
|
3895d5279e | ||
|
|
41be94690f | ||
|
|
3d85ecc525 | ||
|
|
7da00796e5 | ||
|
|
6086419cb6 | ||
|
|
5bc1f2f2c0 | ||
|
|
32a83b211e | ||
|
|
bead7b3a7f | ||
|
|
815d6d6572 | ||
|
|
fbecbee4c3 | ||
|
|
b9a7d2a78c | ||
|
|
95ce812992 | ||
|
|
9a36f4748c | ||
|
|
50b7849a35 | ||
|
|
6f1245b27c | ||
|
|
cc87ed3899 | ||
|
|
1d9037fefe | ||
|
|
03016e2d16 | ||
|
|
bdfb70a58a | ||
|
|
3d41617f4e | ||
|
|
35151ffdd1 | ||
|
|
4527d41a7a | ||
|
|
553cba12f3 | ||
|
|
00fb9c88e1 | ||
|
|
116e068ac3 | ||
|
|
1010dd2d28 | ||
|
|
68bc8302fd | ||
|
|
596dad5cda | ||
|
|
a924c280fb | ||
|
|
7354242906 | ||
|
|
3d0bcf5979 | ||
|
|
e7d0b158e9 | ||
|
|
10ff90787c | ||
|
|
330c4657b1 | ||
|
|
72a109f109 | ||
|
|
cf45c51dfb | ||
|
|
0b013adb34 | ||
|
|
7457d91f64 | ||
|
|
7fe1159426 | ||
|
|
c2665e3677 | ||
|
|
d63de803a4 | ||
|
|
11aca3513c | ||
|
|
561c9f40e5 | ||
|
|
54ed13aadf | ||
|
|
109cc21337 | ||
|
|
7e46b30fa5 | ||
|
|
0ba112c2c7 | ||
|
|
fc15d94170 | ||
|
|
dcb37d9c55 | ||
|
|
755b9d6342 | ||
|
|
3d6151c94f | ||
|
|
590bd8c4b9 | ||
|
|
e99aafd876 | ||
|
|
1f0adf8bcf | ||
|
|
dbd5d5fb43 | ||
|
|
a8b0e3641b | ||
|
|
9efb350be9 | ||
|
|
8d9820b3fb | ||
|
|
103f89551a | ||
|
|
6030d961ad | ||
|
|
ee08c9e17f | ||
|
|
48dd9a3240 | ||
|
|
e122e206a6 | ||
|
|
398b905758 | ||
|
|
dc2ec08fe3 | ||
|
|
3bf5edf5c9 | ||
|
|
134bca526c | ||
|
|
3393e58b06 | ||
|
|
648d7e73c6 | ||
|
|
eab6cdeee4 | ||
|
|
e8ec1ce8e3 | ||
|
|
b3581564ed | ||
|
|
29e1bd95fd | ||
|
|
8bff401c14 | ||
|
|
41798e9255 | ||
|
|
9e4f0228d1 | ||
|
|
76ee93c98c | ||
|
|
fb1a89efb7 | ||
|
|
aface43554 | ||
|
|
a35f0157b2 | ||
|
|
9b32162906 | ||
|
|
21bba62572 | ||
|
|
302327d6b3 | ||
|
|
5667e8bcbb | ||
|
|
ae66bd0e31 | ||
|
|
48dfadc02d | ||
|
|
3df6272bb6 | ||
|
|
e7f9bcda01 | ||
|
|
205044ca66 | ||
|
|
d497eb1f00 | ||
|
|
4e6f970ee9 | ||
|
|
0b6cdda6f5 | ||
|
|
a896ded763 | ||
|
|
fb5dd9ebc2 | ||
|
|
c8b7db6c38 | ||
|
|
44a3191be3 | ||
|
|
b4f7cdc9e7 | ||
|
|
8da07018d5 | ||
|
|
0c19a27065 | ||
|
|
3296b0ecdf | ||
|
|
0a07261124 | ||
|
|
33106d0ecf | ||
|
|
5bb887206a | ||
|
|
b30b0e27cb | ||
|
|
363736489c | ||
|
|
8dbf5e87a0 | ||
|
|
0b30f2cb50 | ||
|
|
ba5265dac4 | ||
|
|
ecb9c65917 | ||
|
|
8a98474600 | ||
|
|
b072216e67 | ||
|
|
cfb3181716 | ||
|
|
ab684cdc99 | ||
|
|
facadc3a44 | ||
|
|
f599bc22d7 | ||
|
|
281319d2da | ||
|
|
5cb203685c | ||
|
|
300c6e7406 | ||
|
|
9c4d6a0773 | ||
|
|
01fa37900b | ||
|
|
edbe744e17 | ||
|
|
2a32a1a4a8 | ||
|
|
404bdb21e6 | ||
|
|
b260c9a512 | ||
|
|
4b941adb6a | ||
|
|
bd752550a8 | ||
|
|
b8b71bb961 | ||
|
|
5aaf7a4092 | ||
|
|
030e02ffb8 | ||
|
|
60746c6253 | ||
|
|
d962aa03f4 | ||
|
|
121a5a1888 | ||
|
|
9e4a2aae43 | ||
|
|
ee6eb685e7 | ||
|
|
09a38a32ce | ||
|
|
d13b19d43d | ||
|
|
5316ec1b4d | ||
|
|
e730dca1ad | ||
|
|
8da30640bb | ||
|
|
6f4eb88e07 | ||
|
|
d9592b9dab | ||
|
|
b87ada72aa | ||
|
|
83363ba1f0 | ||
|
|
a2a7349ce4 | ||
|
|
23ebe7f718 | ||
|
|
e04264cfa3 | ||
|
|
8d29e5037f | ||
|
|
6926ed45b0 | ||
|
|
736b85b8bb | ||
|
|
9e3361bc31 | ||
|
|
6e10381020 | ||
|
|
a1d37d379c | ||
|
|
07d87db7a2 | ||
|
|
4e556673d2 | ||
|
|
f421304fc1 | ||
|
|
6867616973 | ||
|
|
c9271b1686 | ||
|
|
12eb6863da | ||
|
|
4834874091 | ||
|
|
8759ebf200 | ||
|
|
d4715aebef | ||
|
|
0fe2ade7bb | ||
|
|
0c71565535 | ||
|
|
cf8029ecd4 | ||
|
|
6a637091a2 | ||
|
|
31eba60012 | ||
|
|
51e58e9078 | ||
|
|
4a1e76730a | ||
|
|
5599bb028b | ||
|
|
552c6da0cc | ||
|
|
cc6817a891 | ||
|
|
fb48d1b485 | ||
|
|
1c336dad6b | ||
|
|
a4940d46cd | ||
|
|
499b2f44c1 | ||
|
|
2b200c9281 | ||
|
|
36a900c98f | ||
|
|
5236b03f66 | ||
|
|
8be35e3621 | ||
|
|
509f00fe89 | ||
|
|
a98b87f148 | ||
|
|
ae9b2b3b72 | ||
|
|
02e1ec0ae3 | ||
|
|
daefb0f120 | ||
|
|
ff0604e3b6 | ||
|
|
20e41e22fa | ||
|
|
59264c1fd9 | ||
|
|
a0e3bdd594 | ||
|
|
6580aaf3ad | ||
|
|
0b46701b60 | ||
|
|
0bb4effede | ||
|
|
b07082a52d | ||
|
|
04f267f5a7 | ||
|
|
03ccce2804 | ||
|
|
e894bd9f24 | ||
|
|
10e6988273 | ||
|
|
905b61e5d8 | ||
|
|
ee69d393ae | ||
|
|
cab39973ae | ||
|
|
d93f5d07bb | ||
|
|
ba00ffe1ae | ||
|
|
6afaf5eaf5 | ||
|
|
d30459cc34 | ||
|
|
e92fbb7b1b | ||
|
|
42d464b532 | ||
|
|
c2e9e5c63a | ||
|
|
bc36726925 | ||
|
|
22725b0188 | ||
|
|
7abbff8c31 | ||
|
|
6236f4bcf4 | ||
|
|
3c3e80f77f | ||
|
|
4aae2fb289 | ||
|
|
66ff07752f | ||
|
|
5cf92f2742 | ||
|
|
6d3fddc474 | ||
|
|
66d4ad6174 | ||
|
|
2a366a1607 | ||
|
|
d87a0995b4 | ||
|
|
9a73a41e04 | ||
|
|
ba041b36bc | ||
|
|
f5f9de69b4 | ||
|
|
71e56c62e8 | ||
|
|
a0b0c2b963 | ||
|
|
0f496619fd | ||
|
|
5fdd6a441a | ||
|
|
00f287bb63 | ||
|
|
785268efa6 | ||
|
|
2c976d9394 | ||
|
|
1e32582642 | ||
|
|
6f8f6d07f5 | ||
|
|
3958111e76 | ||
|
|
86fcc4af74 | ||
|
|
2fd26756df | ||
|
|
478f4b74d8 | ||
|
|
73d0d2a1bb | ||
|
|
546db08ec4 | ||
|
|
0dd41a8670 | ||
|
|
82c0c89f46 | ||
|
|
f4ce0fd5f1 | ||
|
|
c3798bf4c2 | ||
|
|
ff80b6ccb0 | ||
|
|
e729217116 | ||
|
|
94c695daca | ||
|
|
9f189f0420 | ||
|
|
ad09e53f60 | ||
|
|
092a7a5f3f | ||
|
|
f45649bd25 | ||
|
|
2595cc5ed7 | ||
|
|
2f62190c6f | ||
|
|
577314984c | ||
|
|
f0346b955b | ||
|
|
70139ded4a | ||
|
|
bf379900e1 | ||
|
|
9bafc90f5e | ||
|
|
fce0d9e88e | ||
|
|
2b3b154989 | ||
|
|
948d2440a1 | ||
|
|
5adbe1ce7a | ||
|
|
8157d34ffa | ||
|
|
3ec8cb2204 | ||
|
|
0daa826543 | ||
|
|
a66028da58 | ||
|
|
807c9e6872 | ||
|
|
e71f3774ba | ||
|
|
dd7314bf10 | ||
|
|
f33bc127dc | ||
|
|
db92b87782 | ||
|
|
eba41c8693 | ||
|
|
c855308162 | ||
|
|
73d971bed8 | ||
|
|
bcfe0c2874 | ||
|
|
931ff666ae | ||
|
|
18b6d86cc4 | ||
|
|
086040f858 | ||
|
|
adbeb527d6 | ||
|
|
043176168d | ||
|
|
3c5efa0662 | ||
|
|
9b739bcbbf | ||
|
|
db89076e48 | ||
|
|
19b341ef18 | ||
|
|
be3713b1a3 | ||
|
|
99c4415cfb | ||
|
|
7b311f2ccf | ||
|
|
4aeabfe0a7 | ||
|
|
431ed02194 | ||
|
|
07f587ed83 | ||
|
|
0408341d82 | ||
|
|
5b3c9432f3 | ||
|
|
4a197e63f9 | ||
|
|
ad79a2ef45 | ||
|
|
0876a12fe9 | ||
|
|
c43c7ecc03 | ||
|
|
4a6dee3044 | ||
|
|
019acdd840 | ||
|
|
1c98512720 | ||
|
|
43041cebed | ||
|
|
23a09ad546 | ||
|
|
0836e8fe7c | ||
|
|
90196af8f8 | ||
|
|
002e549a86 | ||
|
|
1de6f859bf | ||
|
|
566fe05772 | ||
|
|
18772c6292 | ||
|
|
6278bddc9b | ||
|
|
f74bf71735 | ||
|
|
efe9ed68b2 | ||
|
|
7c1e75865d | ||
|
|
89530fc4e7 | ||
|
|
a0aee41f1a | ||
|
|
2049dd75f4 | ||
|
|
0864c35ba9 | ||
|
|
92c9f66671 | ||
|
|
223d6dad51 | ||
|
|
815784e809 | ||
|
|
2795d00d1e | ||
|
|
86dd0b4963 | ||
|
|
77a4f4819f | ||
|
|
b63d603482 | ||
|
|
e569b4e613 | ||
|
|
8a70997546 | ||
|
|
80d0a0f882 | ||
|
|
70b3997874 | ||
|
|
e8e4311068 | ||
|
|
cb0fa5829d | ||
|
|
a66f86d4af | ||
|
|
35d98dcea8 | ||
|
|
38fefde06d | ||
|
|
75ecb31f8c | ||
|
|
77133375ad | ||
|
|
c58b93ff51 | ||
|
|
7d8ebfe91b | ||
|
|
810381eab2 | ||
|
|
61dc6cf2de | ||
|
|
0205ebad2a | ||
|
|
09a94133ac | ||
|
|
1eb3c3b219 | ||
|
|
457845bb51 | ||
|
|
0c11b46585 | ||
|
|
c35100d9e9 | ||
|
|
847031cb04 | ||
|
|
d1ca6288a3 | ||
|
|
624ad4cfe6 | ||
|
|
f8d87bb452 | ||
|
|
f60b3505e0 | ||
|
|
addefbc511 | ||
|
|
c4314b25a3 | ||
|
|
921bb86127 | ||
|
|
d912fb0f8b | ||
|
|
e8fc053a32 | ||
|
|
ce3b2bab39 | ||
|
|
15e3699535 | ||
|
|
a4bf6bddbf | ||
|
|
f1b3c6b735 | ||
|
|
e923434d08 | ||
|
|
ddc9cd0fd5 | ||
|
|
d081db0c30 | ||
|
|
14298b0859 | ||
|
|
03ecda3cfe | ||
|
|
350cb767c3 | ||
|
|
f450dcbb57 | ||
|
|
32e003965a | ||
|
|
65f0764338 | ||
|
|
1bdb026079 | ||
|
|
b3a7fb9c3e | ||
|
|
c143c81a7e | ||
|
|
dd389ba0f8 | ||
|
|
46b1649ab8 | ||
|
|
89710412e4 | ||
|
|
931973b632 | ||
|
|
60aaa838e3 | ||
|
|
7e51286313 | ||
|
|
1246538bbb | ||
|
|
80518abf9d | ||
|
|
fc1ae2a18e | ||
|
|
3fd8d2049c | ||
|
|
35a6bcf20c | ||
|
|
0d75fc331e | ||
|
|
0a23e793e3 | ||
|
|
2c1c03e063 | ||
|
|
64059d2949 | ||
|
|
648aa7c4d3 | ||
|
|
274bb81a08 | ||
|
|
e2c90b4681 | ||
|
|
fa0a98ac6e | ||
|
|
e6e7b42415 | ||
|
|
0b7ef2e1d4 | ||
|
|
2fac67a9f9 | ||
|
|
8b9892de2e | ||
|
|
b3290dc909 | ||
|
|
3e3176eddb | ||
|
|
b1ef84894a | ||
|
|
c6cffc92c4 | ||
|
|
efb9fd2712 | ||
|
|
94b294ff93 | ||
|
|
99a9e33648 | ||
|
|
055d94a919 | ||
|
|
0978005240 | ||
|
|
1f796581ec | ||
|
|
f3a1716dad | ||
|
|
a1c3a0db1f | ||
|
|
9f80cc8a6b | ||
|
|
133786846e | ||
|
|
bdf297a5c6 | ||
|
|
6767254eb0 | ||
|
|
691cebd479 | ||
|
|
f3932cbf29 | ||
|
|
3f73a97037 | ||
|
|
226f1f5be4 | ||
|
|
7e45c07660 | ||
|
|
0c815036b9 |
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- manager-v4
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
@@ -31,28 +31,28 @@ jobs:
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
CURRENT_VERSION=$(grep -oP 'version = "\K[^"]+' pyproject.toml)
|
||||
CURRENT_VERSION=$(grep -oP '^version = "\K[^"]+' pyproject.toml)
|
||||
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
files: dist/*
|
||||
tag_name: v${{ steps.current_version.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
# - name: Create GitHub Release
|
||||
# id: create_release
|
||||
# uses: softprops/action-gh-release@v2
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ github.token }}
|
||||
# with:
|
||||
# files: dist/*
|
||||
# tag_name: v${{ steps.current_version.outputs.version }}
|
||||
# draft: false
|
||||
# prerelease: false
|
||||
# generate_release_notes: true
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
skip-existing: true
|
||||
verbose: true
|
||||
verbose: true
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main-blocked
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ltdrdata' || github.repository_owner == 'Comfy-Org' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
@@ -89,20 +89,20 @@
|
||||
|
||||
|
||||
## Paths
|
||||
In `ComfyUI-Manager` V3.0 and later, configuration files and dynamically generated files are located under `<USER_DIRECTORY>/default/ComfyUI-Manager/`.
|
||||
In `ComfyUI-Manager` V4.0.3b4 and later, configuration files and dynamically generated files are located under `<USER_DIRECTORY>/__manager/`.
|
||||
|
||||
* <USER_DIRECTORY>
|
||||
* If executed without any options, the path defaults to ComfyUI/user.
|
||||
* It can be set using --user-directory <USER_DIRECTORY>.
|
||||
* <USER_DIRECTORY>
|
||||
* If executed without any options, the path defaults to ComfyUI/user.
|
||||
* It can be set using --user-directory <USER_DIRECTORY>.
|
||||
|
||||
* Basic config files: `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini`
|
||||
* Configurable channel lists: `<USER_DIRECTORY>/default/ComfyUI-Manager/channels.ini`
|
||||
* 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`
|
||||
* Startup script files: `<USER_DIRECTORY>/default/ComfyUI-Manager/startup-scripts`
|
||||
* Component files: `<USER_DIRECTORY>/default/ComfyUI-Manager/components`
|
||||
* Basic config files: `<USER_DIRECTORY>/__manager/config.ini`
|
||||
* Configurable channel lists: `<USER_DIRECTORY>/__manager/channels.ini`
|
||||
* Configurable pip overrides: `<USER_DIRECTORY>/__manager/pip_overrides.json`
|
||||
* Configurable pip blacklist: `<USER_DIRECTORY>/__manager/pip_blacklist.list`
|
||||
* Configurable pip auto fix: `<USER_DIRECTORY>/__manager/pip_auto_fix.list`
|
||||
* Saved snapshot files: `<USER_DIRECTORY>/__manager/snapshots`
|
||||
* Startup script files: `<USER_DIRECTORY>/__manager/startup-scripts`
|
||||
* Component files: `<USER_DIRECTORY>/__manager/components`
|
||||
|
||||
|
||||
## `extra_model_paths.yaml` Configuration
|
||||
@@ -115,17 +115,17 @@ The following settings are applied based on the section marked as `is_default`.
|
||||
|
||||
## Snapshot-Manager
|
||||
* When you press `Save snapshot` or use `Update All` on `Manager Menu`, the current installation status snapshot is saved.
|
||||
* Snapshot file dir: `<USER_DIRECTORY>/default/ComfyUI-Manager/snapshots`
|
||||
* Snapshot file dir: `<USER_DIRECTORY>/__manager/snapshots`
|
||||
* You can rename snapshot file.
|
||||
* Press the "Restore" button to revert to the installation status of the respective snapshot.
|
||||
* However, for custom nodes not managed by Git, snapshot support is incomplete.
|
||||
* When you press `Restore`, it will take effect on the next ComfyUI startup.
|
||||
* The selected snapshot file is saved in `<USER_DIRECTORY>/default/ComfyUI-Manager/startup-scripts/restore-snapshot.json`, and upon restarting ComfyUI, the snapshot is applied and then deleted.
|
||||
* The selected snapshot file is saved in `<USER_DIRECTORY>/__manager/startup-scripts/restore-snapshot.json`, and upon restarting ComfyUI, the snapshot is applied and then deleted.
|
||||
|
||||

|
||||
|
||||
|
||||
## cm-cli: command line tools for power user
|
||||
## cm-cli: command line tools for power users
|
||||
* A tool is provided that allows you to use the features of ComfyUI-Manager without running ComfyUI.
|
||||
* For more details, please refer to the [cm-cli documentation](docs/en/cm-cli.md).
|
||||
|
||||
@@ -169,12 +169,12 @@ The following settings are applied based on the section marked as `is_default`.
|
||||
}
|
||||
```
|
||||
* `<current timestamp>` Ensure that the timestamp is always unique.
|
||||
* "components" should have the same structure as the content of the file stored in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
|
||||
* "components" should have the same structure as the content of the file stored in `<USER_DIRECTORY>/__manager/components`.
|
||||
* `<component name>`: The name should be in the format `<prefix>::<node name>`.
|
||||
* `<compnent nodeata>`: In the nodedata of the group node.
|
||||
* `<component node data>`: In the node data of the group node.
|
||||
* `<version>`: Only two formats are allowed: `major.minor.patch` or `major.minor`. (e.g. `1.0`, `2.2.1`)
|
||||
* `<datetime>`: Saved time
|
||||
* `<packname>`: If the packname is not empty, the category becomes packname/workflow, and it is saved in the <packname>.pack file in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
|
||||
* `<packname>`: If the packname is not empty, the category becomes packname/workflow, and it is saved in the <packname>.pack file in `<USER_DIRECTORY>/__manager/components`.
|
||||
* `<category>`: If there is neither a category nor a packname, it is saved in the components category.
|
||||
```
|
||||
"version":"1.0",
|
||||
@@ -189,7 +189,7 @@ The following settings are applied based on the section marked as `is_default`.
|
||||
* Dragging and dropping or pasting a single component will add a node. However, when adding multiple components, nodes will not be added.
|
||||
|
||||
|
||||
## Support of missing nodes installation
|
||||
## Support for installing missing nodes
|
||||
|
||||

|
||||
|
||||
@@ -215,23 +215,24 @@ The following settings are applied based on the section marked as `is_default`.
|
||||
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 = <Set the network mode => public|private|offline|personal_cloud>
|
||||
```
|
||||
|
||||
* 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)
|
||||
- personal_cloud: Applies relaxed security features in cloud environments such as Google Colab or Runpod, where strong security is not required.
|
||||
|
||||
|
||||
## Additional Feature
|
||||
* Logging to file feature
|
||||
* This feature is enabled by default and can be disabled by setting `file_logging = False` in the `config.ini`.
|
||||
|
||||
* Fix node(recreate): When right-clicking on a node and selecting `Fix node (recreate)`, you can recreate the node. The widget's values are reset, while the connections maintain those with the same names.
|
||||
* Fix node (recreate): When right-clicking on a node and selecting `Fix node (recreate)`, you can recreate the node. The widget's values are reset, while the connections maintain those with the same names.
|
||||
* It is used to correct errors in nodes of old workflows created before, which are incompatible with the version changes of custom nodes.
|
||||
|
||||
* Double-Click Node Title: You can set the double click behavior of nodes in the ComfyUI-Manager menu.
|
||||
* Double-Click Node Title: You can set the double-click behavior of nodes in the ComfyUI-Manager menu.
|
||||
* `Copy All Connections`, `Copy Input Connections`: Double-clicking a node copies the connections of the nearest node.
|
||||
* This action targets the nearest node within a straight-line distance of 1000 pixels from the center of the node.
|
||||
* In the case of `Copy All Connections`, it duplicates existing outputs, but since it does not allow duplicate connections, the existing output connections of the original node are disconnected.
|
||||
@@ -297,46 +298,48 @@ When you run the `scan.sh` script:
|
||||
|
||||
* It updates the `github-stats.json`.
|
||||
* This uses the GitHub API, so set your token with `export GITHUB_TOKEN=your_token_here` to avoid quickly reaching the rate limit and malfunctioning.
|
||||
* To skip this step, add the `--skip-update-stat` option.
|
||||
* To skip this step, add the `--skip-stat-update` option.
|
||||
|
||||
* The `--skip-all` option applies both `--skip-update` and `--skip-stat-update`.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
* If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini` file that is generated.
|
||||
* If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the `<USER_DIRECTORY>/__manager/config.ini` file that is generated.
|
||||
* If updating ComfyUI-Manager itself fails, please go to the **ComfyUI-Manager** directory and execute the command `git update-ref refs/remotes/origin/main a361cc1 && git fetch --all && git pull`.
|
||||
* If you encounter the error message `Overlapped Object has pending operation at deallocation on Comfyui Manager load` under Windows
|
||||
* If you encounter the error message `Overlapped Object has pending operation at deallocation on ComfyUI Manager load` under Windows
|
||||
* Edit `config.ini` file: add `windows_selector_event_loop_policy = True`
|
||||
* if `SSL: CERTIFICATE_VERIFY_FAILED` error is occured.
|
||||
* If the `SSL: CERTIFICATE_VERIFY_FAILED` error occurs.
|
||||
* Edit `config.ini` file: add `bypass_ssl = True`
|
||||
|
||||
|
||||
## Security policy
|
||||
* Edit `config.ini` file: add `security_level = <LEVEL>`
|
||||
* `strong`
|
||||
* doesn't allow `high` and `middle` level risky feature
|
||||
* `normal`
|
||||
* doesn't allow `high` level risky feature
|
||||
* `middle` level risky feature is available
|
||||
* `normal-`
|
||||
* doesn't allow `high` level risky feature if `--listen` is specified and not starts with `127.`
|
||||
* `middle` level risky feature is available
|
||||
* `weak`
|
||||
* all feature is available
|
||||
|
||||
* `high` level risky features
|
||||
* `Install via git url`, `pip install`
|
||||
* Installation of custom nodes registered not in the `default channel`.
|
||||
* Fix custom nodes
|
||||
|
||||
* `middle` level risky features
|
||||
* Uninstall/Update
|
||||
* Installation of custom nodes registered in the `default channel`.
|
||||
* Restore/Remove Snapshot
|
||||
* Restart
|
||||
|
||||
* `low` level risky features
|
||||
* Update ComfyUI
|
||||
|
||||
The security settings are applied based on whether the ComfyUI server's listener is non-local and whether the network mode is set to `personal_cloud`.
|
||||
|
||||
* **non-local**: When the server is launched with `--listen` and is bound to a network range other than the local `127.` range, allowing remote IP access.
|
||||
* **personal\_cloud**: When the `network_mode` is set to `personal_cloud`.
|
||||
|
||||
|
||||
### Risky Level Table
|
||||
|
||||
| Risky Level | features |
|
||||
|-------------|---------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| high+ | * `Install via git url`, `pip install`<BR>* Installation of nodepack registered not in the `default channel`. |
|
||||
| high | * Fix nodepack |
|
||||
| middle+ | * Uninstall/Update<BR>* Installation of nodepack registered in the `default channel`.<BR>* Restore/Remove Snapshot<BR>* Install model |
|
||||
| middle | * Restart |
|
||||
| low | * Update ComfyUI |
|
||||
|
||||
|
||||
### Security Level Table
|
||||
|
||||
| Security Level | local | non-local (personal_cloud) | non-local (not personal_cloud) |
|
||||
|----------------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|--------------------------------|
|
||||
| strong | * Only `weak` level risky features are allowed | * Only `weak` level risky features are allowed | * Only `weak` level risky features are allowed |
|
||||
| normal | * `high+` and `high` level risky features are not allowed<BR>* `middle+` and `middle` level risky features are available | * `high+` and `high` level risky features are not allowed<BR>* `middle+` and `middle` level risky features are available | * `high+`, `high` and `middle+` level risky features are not allowed<BR>* `middle` level risky features are available
|
||||
| normal- | * All features are available | * `high+` and `high` level risky features are not allowed<BR>* `middle+` and `middle` level risky features are available | * `high+`, `high` and `middle+` level risky features are not allowed<BR>* `middle` level risky features are available
|
||||
| weak | * All features are available | * All features are available | * `high+` and `middle+` level risky features are not allowed<BR>* `high`, `middle` and `low` level risky features are available
|
||||
|
||||
|
||||
|
||||
# Disclaimer
|
||||
|
||||
@@ -37,7 +37,7 @@ find ~/.tmp/default -name "*.py" -print0 | xargs -0 grep -E "crypto|^_A="
|
||||
|
||||
echo
|
||||
echo CHECK3
|
||||
find ~/.tmp/default -name "requirements.txt" | xargs grep "^\s*https\\?:"
|
||||
find ~/.tmp/default -name "requirements.txt" | xargs grep "\.whl"
|
||||
find ~/.tmp/default -name "requirements.txt" | xargs grep "^\s*[^#]*https\?:"
|
||||
find ~/.tmp/default -name "requirements.txt" | xargs grep "^\s*[^#].*\.whl"
|
||||
|
||||
echo
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import os
|
||||
import logging
|
||||
from aiohttp import web
|
||||
from .common.manager_security import HANDLER_POLICY
|
||||
from .common import manager_security
|
||||
from comfy.cli_args import args
|
||||
|
||||
|
||||
def prestartup():
|
||||
from . import prestartup_script # noqa: F401
|
||||
@@ -7,25 +12,29 @@ def prestartup():
|
||||
|
||||
|
||||
def start():
|
||||
from comfy.cli_args import args
|
||||
|
||||
logging.info('[START] ComfyUI-Manager')
|
||||
from .common import cm_global # noqa: F401
|
||||
|
||||
if not args.disable_manager:
|
||||
if args.enable_manager:
|
||||
if args.enable_manager_legacy_ui:
|
||||
try:
|
||||
from .legacy import manager_server # noqa: F401
|
||||
from .legacy import share_3rdparty # noqa: F401
|
||||
from .legacy import manager_core as core
|
||||
import nodes
|
||||
|
||||
logging.info("[ComfyUI-Manager] Legacy UI is enabled.")
|
||||
nodes.EXTENSION_WEB_DIRS['comfyui-manager-legacy'] = os.path.join(os.path.dirname(__file__), 'js')
|
||||
except Exception as e:
|
||||
print("Error enabling legacy ComfyUI Manager frontend:", e)
|
||||
core = None
|
||||
else:
|
||||
from .glob import manager_server # noqa: F401
|
||||
from .glob import share_3rdparty # noqa: F401
|
||||
from .glob import manager_core as core
|
||||
|
||||
if core is not None:
|
||||
manager_security.is_personal_cloud_mode = core.get_config()['network_mode'].lower() == 'personal_cloud'
|
||||
|
||||
|
||||
def should_be_disabled(fullpath:str) -> bool:
|
||||
@@ -33,9 +42,7 @@ def should_be_disabled(fullpath:str) -> bool:
|
||||
1. Disables the legacy ComfyUI-Manager.
|
||||
2. The blocklist can be expanded later based on policies.
|
||||
"""
|
||||
from comfy.cli_args import args
|
||||
|
||||
if not args.disable_manager:
|
||||
if args.enable_manager:
|
||||
# In cases where installation is done via a zip archive, the directory name may not be comfyui-manager, and it may not contain a git repository.
|
||||
# It is assumed that any installed legacy ComfyUI-Manager will have at least 'comfyui-manager' in its directory name.
|
||||
dir_name = os.path.basename(fullpath).lower()
|
||||
@@ -43,3 +50,56 @@ def should_be_disabled(fullpath:str) -> bool:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_client_ip(request):
|
||||
peername = request.transport.get_extra_info("peername")
|
||||
if peername is not None:
|
||||
# Grab the first two values - there can be more, ie. with --listen
|
||||
host, port = peername[:2]
|
||||
return host
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def create_middleware():
|
||||
connected_clients = set()
|
||||
is_local_mode = manager_security.is_loopback(args.listen)
|
||||
|
||||
@web.middleware
|
||||
async def manager_middleware(request: web.Request, handler):
|
||||
nonlocal connected_clients
|
||||
|
||||
# security policy for remote environments
|
||||
prev_client_count = len(connected_clients)
|
||||
client_ip = get_client_ip(request)
|
||||
connected_clients.add(client_ip)
|
||||
next_client_count = len(connected_clients)
|
||||
|
||||
if prev_client_count == 1 and next_client_count > 1:
|
||||
manager_security.multiple_remote_alert()
|
||||
|
||||
policy = manager_security.get_handler_policy(handler)
|
||||
is_banned = False
|
||||
|
||||
# policy check
|
||||
if len(connected_clients) > 1:
|
||||
if is_local_mode:
|
||||
if HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NON_LOCAL in policy:
|
||||
is_banned = True
|
||||
if HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD in policy:
|
||||
is_banned = not manager_security.is_personal_cloud_mode
|
||||
|
||||
if HANDLER_POLICY.BANNED in policy:
|
||||
is_banned = True
|
||||
|
||||
if is_banned:
|
||||
logging.warning(f"[Manager] Banning request from {client_ip}: {request.path}")
|
||||
response = web.Response(text="[Manager] This request is banned.", status=403)
|
||||
else:
|
||||
response: web.Response = await handler(request)
|
||||
|
||||
return response
|
||||
|
||||
return manager_middleware
|
||||
|
||||
@@ -46,10 +46,7 @@ comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
|
||||
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
|
||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||
else:
|
||||
cm_global.pip_overrides = {}
|
||||
cm_global.pip_overrides = {}
|
||||
|
||||
if os.path.exists(os.path.join(manager_util.comfyui_manager_path, "pip_overrides.json")):
|
||||
with open(os.path.join(manager_util.comfyui_manager_path, "pip_overrides.json"), 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
@@ -152,9 +149,6 @@ class Ctx:
|
||||
with open(context.manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
cm_global.pip_overrides = json.load(json_file)
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||
|
||||
if os.path.exists(context.manager_pip_blacklist_path):
|
||||
with open(context.manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
|
||||
for x in f.readlines():
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from .timestamp_utils import (
|
||||
current_timestamp,
|
||||
get_timestamp_for_filename,
|
||||
get_timestamp_for_path,
|
||||
get_backup_branch_name,
|
||||
get_now,
|
||||
get_unix_timestamp,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'current_timestamp',
|
||||
'get_timestamp_for_filename',
|
||||
'get_timestamp_for_path',
|
||||
'get_backup_branch_name',
|
||||
'get_now',
|
||||
'get_unix_timestamp',
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@ from . import manager_util
|
||||
|
||||
import requests
|
||||
import toml
|
||||
import logging
|
||||
|
||||
base_url = "https://api.comfy.org"
|
||||
|
||||
@@ -23,7 +24,7 @@ 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.")
|
||||
logging.info("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):
|
||||
@@ -68,7 +69,10 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
|
||||
form_factor = 'git-linux'
|
||||
else:
|
||||
form_factor = 'other'
|
||||
|
||||
|
||||
from comfyui_manager.glob import manager_core
|
||||
verbose = manager_core.get_config().get('verbose', False)
|
||||
|
||||
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}'
|
||||
@@ -78,13 +82,13 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
|
||||
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']}")
|
||||
if page % 5 == 0 and verbose:
|
||||
logging.info(f"FETCH ComfyRegistry Data: {page}/{sub_json_obj['totalPages']}")
|
||||
|
||||
page += 1
|
||||
time.sleep(0.5)
|
||||
|
||||
print("FETCH ComfyRegistry Data [DONE]")
|
||||
logging.info("FETCH ComfyRegistry Data [DONE]")
|
||||
|
||||
for v in full_nodes.values():
|
||||
if 'latest_version' not in v:
|
||||
@@ -100,7 +104,7 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
|
||||
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.")
|
||||
logging.info("[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']
|
||||
|
||||
@@ -114,7 +118,7 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
|
||||
return json_obj['nodes']
|
||||
except Exception:
|
||||
res = {}
|
||||
print("Cannot connect to comfyregistry.")
|
||||
logging.warning("Cannot connect to comfyregistry.")
|
||||
finally:
|
||||
if cache_mode:
|
||||
is_cache_loading = False
|
||||
@@ -180,7 +184,7 @@ def install_node(node_id, version=None):
|
||||
else:
|
||||
url = f"{base_url}/nodes/{node_id}/install?version={version}"
|
||||
|
||||
response = requests.get(url)
|
||||
response = requests.get(url, verify=not manager_util.bypass_ssl)
|
||||
if response.status_code == 200:
|
||||
# Convert the API response to a NodeVersion object
|
||||
return map_node_version(response.json())
|
||||
@@ -191,7 +195,7 @@ def install_node(node_id, version=None):
|
||||
def all_versions_of_node(node_id):
|
||||
url = f"{base_url}/nodes/{node_id}/versions?statuses=NodeVersionStatusActive&statuses=NodeVersionStatusPending"
|
||||
|
||||
response = requests.get(url)
|
||||
response = requests.get(url, verify=not manager_util.bypass_ssl)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
@@ -211,6 +215,7 @@ def read_cnr_info(fullpath):
|
||||
|
||||
project = data.get('project', {})
|
||||
name = project.get('name').strip().lower()
|
||||
original_name = project.get('name')
|
||||
|
||||
# normalize version
|
||||
# for example: 2.5 -> 2.5.0
|
||||
@@ -222,6 +227,7 @@ def read_cnr_info(fullpath):
|
||||
if name and version: # repository is optional
|
||||
return {
|
||||
"id": name,
|
||||
"original_name": original_name,
|
||||
"version": version,
|
||||
"url": repository
|
||||
}
|
||||
@@ -238,7 +244,7 @@ def generate_cnr_id(fullpath, cnr_id):
|
||||
with open(cnr_id_path, "w") as f:
|
||||
return f.write(cnr_id)
|
||||
except Exception:
|
||||
print(f"[ComfyUI Manager] unable to create file: {cnr_id_path}")
|
||||
logging.error(f"[ComfyUI Manager] unable to create file: {cnr_id_path}")
|
||||
|
||||
|
||||
def read_cnr_id(fullpath):
|
||||
|
||||
@@ -22,7 +22,7 @@ if comfy_base_path is None:
|
||||
comfy_base_path = comfy_path
|
||||
|
||||
channel_list_template_path = os.path.join(manager_util.comfyui_manager_path, 'channels.list.template')
|
||||
git_script_path = os.path.join(manager_util.comfyui_manager_path, "git_helper.py")
|
||||
git_script_path = os.path.join(manager_util.comfyui_manager_path, "common", "git_helper.py")
|
||||
|
||||
manager_files_path = None
|
||||
manager_config_path = None
|
||||
@@ -31,10 +31,9 @@ manager_startup_script_path:str = None
|
||||
manager_snapshot_path = None
|
||||
manager_pip_overrides_path = None
|
||||
manager_pip_blacklist_path = None
|
||||
manager_components_path = None
|
||||
manager_batch_history_path = None
|
||||
|
||||
def update_user_directory(user_dir):
|
||||
def update_user_directory(manager_dir):
|
||||
global manager_files_path
|
||||
global manager_config_path
|
||||
global manager_channel_list_path
|
||||
@@ -42,10 +41,9 @@ def update_user_directory(user_dir):
|
||||
global manager_snapshot_path
|
||||
global manager_pip_overrides_path
|
||||
global manager_pip_blacklist_path
|
||||
global manager_components_path
|
||||
global manager_batch_history_path
|
||||
|
||||
manager_files_path = os.path.abspath(os.path.join(user_dir, 'default', 'ComfyUI-Manager'))
|
||||
manager_files_path = manager_dir
|
||||
if not os.path.exists(manager_files_path):
|
||||
os.makedirs(manager_files_path)
|
||||
|
||||
@@ -61,7 +59,6 @@ def update_user_directory(user_dir):
|
||||
manager_channel_list_path = os.path.join(manager_files_path, 'channels.list')
|
||||
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")
|
||||
manager_components_path = os.path.join(manager_files_path, "components")
|
||||
manager_util.cache_dir = os.path.join(manager_files_path, "cache")
|
||||
manager_batch_history_path = os.path.join(manager_files_path, "batch_history")
|
||||
|
||||
@@ -73,7 +70,7 @@ def update_user_directory(user_dir):
|
||||
|
||||
try:
|
||||
import folder_paths
|
||||
update_user_directory(folder_paths.get_user_directory())
|
||||
update_user_directory(folder_paths.get_system_user_directory("manager"))
|
||||
|
||||
except Exception:
|
||||
# fallback:
|
||||
@@ -106,4 +103,3 @@ def get_comfyui_tag():
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ class NetworkMode(enum.Enum):
|
||||
PUBLIC = "public"
|
||||
PRIVATE = "private"
|
||||
OFFLINE = "offline"
|
||||
PERSONAL_CLOUD = "personal_cloud"
|
||||
|
||||
class SecurityLevel(enum.Enum):
|
||||
STRONG = "strong"
|
||||
|
||||
@@ -9,6 +9,7 @@ import yaml
|
||||
import requests
|
||||
from tqdm.auto import tqdm
|
||||
from git.remote import RemoteProgress
|
||||
from comfyui_manager.common.timestamp_utils import get_backup_branch_name
|
||||
|
||||
|
||||
comfy_path = os.environ.get('COMFYUI_PATH')
|
||||
@@ -222,7 +223,14 @@ def gitpull(path):
|
||||
repo.close()
|
||||
return
|
||||
|
||||
remote.pull()
|
||||
try:
|
||||
repo.git.pull('--ff-only')
|
||||
except git.GitCommandError:
|
||||
backup_name = get_backup_branch_name(repo)
|
||||
repo.create_head(backup_name)
|
||||
print(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
|
||||
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
|
||||
print(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
|
||||
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
new_commit_hash = repo.head.commit.hexsha
|
||||
|
||||
@@ -74,6 +74,12 @@ def normalize_to_github_id(url) -> str:
|
||||
|
||||
return f"{author}/{repo_name}"
|
||||
|
||||
# Handle short format like "author/repo" (aux_id format)
|
||||
if '/' in url and not url.startswith('http'):
|
||||
parts = url.split('/')
|
||||
if len(parts) == 2 and parts[0] and parts[1]:
|
||||
return url
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,11 @@ def download_url(model_url: str, model_dir: str, filename: str):
|
||||
return aria2_download_url(model_url, model_dir, filename)
|
||||
else:
|
||||
from torchvision.datasets.utils import download_url as torchvision_download_url
|
||||
return torchvision_download_url(model_url, model_dir, filename)
|
||||
try:
|
||||
return torchvision_download_url(model_url, model_dir, filename)
|
||||
except Exception as e:
|
||||
logging.error(f"[ComfyUI-Manager] Failed to download: {model_url} / {repr(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def aria2_find_task(dir: str, filename: str):
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from enum import Enum
|
||||
|
||||
is_personal_cloud_mode = False
|
||||
handler_policy = {}
|
||||
|
||||
class HANDLER_POLICY(Enum):
|
||||
MULTIPLE_REMOTE_BAN_NON_LOCAL = 1
|
||||
MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD = 2
|
||||
BANNED = 3
|
||||
|
||||
|
||||
def is_loopback(address):
|
||||
import ipaddress
|
||||
try:
|
||||
return ipaddress.ip_address(address).is_loopback
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def do_nothing():
|
||||
pass
|
||||
|
||||
|
||||
def get_handler_policy(x):
|
||||
return handler_policy.get(x) or set()
|
||||
|
||||
def add_handler_policy(x, policy):
|
||||
s = handler_policy.get(x)
|
||||
if s is None:
|
||||
s = set()
|
||||
handler_policy[x] = s
|
||||
|
||||
s.add(policy)
|
||||
|
||||
|
||||
multiple_remote_alert = do_nothing
|
||||
@@ -8,14 +8,14 @@ import aiohttp
|
||||
import json
|
||||
import threading
|
||||
import os
|
||||
from datetime import datetime
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
import logging
|
||||
import platform
|
||||
import shlex
|
||||
from . import cm_global
|
||||
import time
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
cache_lock = threading.Lock()
|
||||
@@ -25,6 +25,7 @@ comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '
|
||||
cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also updated together in **manager_core.update_user_directory**.
|
||||
|
||||
use_uv = False
|
||||
bypass_ssl = False
|
||||
|
||||
def is_manager_pip_package():
|
||||
return not os.path.exists(os.path.join(comfyui_manager_path, '..', 'custom_nodes'))
|
||||
@@ -38,18 +39,64 @@ def add_python_path_to_env():
|
||||
os.environ['PATH'] = os.path.dirname(sys.executable)+sep+os.environ['PATH']
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def get_pip_cmd(force_uv=False):
|
||||
"""
|
||||
Get the base pip command, with automatic fallback to uv if pip is unavailable.
|
||||
|
||||
Args:
|
||||
force_uv (bool): If True, use uv directly without trying pip
|
||||
|
||||
Returns:
|
||||
list: Base command for pip operations
|
||||
"""
|
||||
embedded = 'python_embeded' in sys.executable
|
||||
|
||||
# Try pip first (unless forcing uv)
|
||||
if not force_uv:
|
||||
try:
|
||||
test_cmd = [sys.executable] + (['-s'] if embedded else []) + ['-m', 'pip', '--version']
|
||||
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
|
||||
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'pip']
|
||||
except Exception:
|
||||
logging.warning("[ComfyUI-Manager] python -m pip not available. Falling back to uv.")
|
||||
|
||||
# Try uv (either forced or pip failed)
|
||||
import shutil
|
||||
|
||||
# Try uv as Python module
|
||||
try:
|
||||
test_cmd = [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', '--version']
|
||||
subprocess.check_output(test_cmd, stderr=subprocess.DEVNULL, timeout=5)
|
||||
logging.info("[ComfyUI-Manager] Using uv as Python module for pip operations.")
|
||||
return [sys.executable] + (['-s'] if embedded else []) + ['-m', 'uv', 'pip']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try standalone uv
|
||||
if shutil.which('uv'):
|
||||
logging.info("[ComfyUI-Manager] Using standalone uv for pip operations.")
|
||||
return ['uv', 'pip']
|
||||
|
||||
# Nothing worked
|
||||
logging.error("[ComfyUI-Manager] Neither python -m pip nor uv are available. Cannot proceed with package operations.")
|
||||
raise Exception("Neither pip nor uv are available for package management")
|
||||
|
||||
|
||||
def make_pip_cmd(cmd):
|
||||
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
|
||||
"""
|
||||
Create a pip command by combining the cached base pip command with the given arguments.
|
||||
|
||||
Args:
|
||||
cmd (list): List of pip command arguments (e.g., ['install', 'package'])
|
||||
|
||||
Returns:
|
||||
list: Complete command list ready for subprocess execution
|
||||
"""
|
||||
global use_uv
|
||||
base_cmd = get_pip_cmd(force_uv=use_uv)
|
||||
return base_cmd + cmd
|
||||
|
||||
|
||||
# DON'T USE StrictVersion - cannot handle pre_release version
|
||||
# try:
|
||||
@@ -129,7 +176,7 @@ def is_file_created_within_one_day(file_path):
|
||||
return False
|
||||
|
||||
file_creation_time = os.path.getctime(file_path)
|
||||
current_time = datetime.now().timestamp()
|
||||
current_time = time.time()
|
||||
time_difference = current_time - file_creation_time
|
||||
|
||||
return time_difference <= 86400
|
||||
@@ -140,7 +187,7 @@ async def get_data(uri, silent=False):
|
||||
print(f"FETCH DATA from: {uri}", end="")
|
||||
|
||||
if uri.startswith("http"):
|
||||
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
|
||||
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=not bypass_ssl)) as session:
|
||||
headers = {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
@@ -330,6 +377,32 @@ torch_torchvision_torchaudio_version_map = {
|
||||
}
|
||||
|
||||
|
||||
def torch_rollback(prev):
|
||||
spec = prev.split('+')
|
||||
if len(spec) > 1:
|
||||
platform = spec[1]
|
||||
else:
|
||||
cmd = make_pip_cmd(['install', '--force', 'torch', 'torchvision', 'torchaudio'])
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
logging.error(cmd)
|
||||
return
|
||||
|
||||
torch_ver = StrictVersion(spec[0])
|
||||
torch_ver = f"{torch_ver.major}.{torch_ver.minor}.{torch_ver.patch}"
|
||||
torch_torchvision_torchaudio_ver = torch_torchvision_torchaudio_version_map.get(torch_ver)
|
||||
|
||||
if torch_torchvision_torchaudio_ver is None:
|
||||
cmd = make_pip_cmd(['install', '--pre', 'torch', 'torchvision', 'torchaudio',
|
||||
'--index-url', f"https://download.pytorch.org/whl/nightly/{platform}"])
|
||||
logging.info("[ComfyUI-Manager] restore PyTorch to nightly version")
|
||||
else:
|
||||
torchvision_ver, torchaudio_ver = torch_torchvision_torchaudio_ver
|
||||
cmd = make_pip_cmd(['install', f'torch=={torch_ver}', f'torchvision=={torchvision_ver}', f"torchaudio=={torchaudio_ver}",
|
||||
'--index-url', f"https://download.pytorch.org/whl/{platform}"])
|
||||
logging.info(f"[ComfyUI-Manager] restore PyTorch to {torch_ver}+{platform}")
|
||||
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
|
||||
|
||||
class PIPFixer:
|
||||
def __init__(self, prev_pip_versions, comfyui_path, manager_files_path):
|
||||
@@ -337,32 +410,6 @@ class PIPFixer:
|
||||
self.comfyui_path = comfyui_path
|
||||
self.manager_files_path = manager_files_path
|
||||
|
||||
def torch_rollback(self):
|
||||
spec = self.prev_pip_versions['torch'].split('+')
|
||||
if len(spec) > 0:
|
||||
platform = spec[1]
|
||||
else:
|
||||
cmd = make_pip_cmd(['install', '--force', 'torch', 'torchvision', 'torchaudio'])
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
logging.error(cmd)
|
||||
return
|
||||
|
||||
torch_ver = StrictVersion(spec[0])
|
||||
torch_ver = f"{torch_ver.major}.{torch_ver.minor}.{torch_ver.patch}"
|
||||
torch_torchvision_torchaudio_ver = torch_torchvision_torchaudio_version_map.get(torch_ver)
|
||||
|
||||
if torch_torchvision_torchaudio_ver is None:
|
||||
cmd = make_pip_cmd(['install', '--pre', 'torch', 'torchvision', 'torchaudio',
|
||||
'--index-url', f"https://download.pytorch.org/whl/nightly/{platform}"])
|
||||
logging.info("[ComfyUI-Manager] restore PyTorch to nightly version")
|
||||
else:
|
||||
torchvision_ver, torchaudio_ver = torch_torchvision_torchaudio_ver
|
||||
cmd = make_pip_cmd(['install', f'torch=={torch_ver}', f'torchvision=={torchvision_ver}', f"torchaudio=={torchaudio_ver}",
|
||||
'--index-url', f"https://download.pytorch.org/whl/{platform}"])
|
||||
logging.info(f"[ComfyUI-Manager] restore PyTorch to {torch_ver}+{platform}")
|
||||
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
|
||||
def fix_broken(self):
|
||||
new_pip_versions = get_installed_packages(True)
|
||||
|
||||
@@ -384,7 +431,7 @@ class PIPFixer:
|
||||
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['torchaudio'] != new_pip_versions['torchaudio']:
|
||||
self.torch_rollback()
|
||||
torch_rollback(self.prev_pip_versions['torch'])
|
||||
except Exception as e:
|
||||
logging.error("[ComfyUI-Manager] Failed to restore PyTorch")
|
||||
logging.error(e)
|
||||
@@ -415,32 +462,14 @@ class PIPFixer:
|
||||
|
||||
if len(targets) > 0:
|
||||
for x in targets:
|
||||
if sys.version_info < (3, 13):
|
||||
cmd = make_pip_cmd(['install', f"{x}=={versions[0].version_string}", "numpy<2"])
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
cmd = make_pip_cmd(['install', f"{x}=={versions[0].version_string}"])
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
|
||||
logging.info(f"[ComfyUI-Manager] 'opencv' dependencies were fixed: {targets}")
|
||||
except Exception as e:
|
||||
logging.error("[ComfyUI-Manager] Failed to restore opencv")
|
||||
logging.error(e)
|
||||
|
||||
# fix numpy
|
||||
if sys.version_info >= (3, 13):
|
||||
logging.info("[ComfyUI-Manager] In Python 3.13 and above, PIP Fixer does not downgrade `numpy` below version 2.0. If you need to force a downgrade of `numpy`, please use `pip_auto_fix.list`.")
|
||||
else:
|
||||
try:
|
||||
np = new_pip_versions.get('numpy')
|
||||
if cm_global.pip_overrides.get('numpy') == 'numpy<2':
|
||||
if np is not None:
|
||||
if StrictVersion(np) >= StrictVersion('2'):
|
||||
cmd = make_pip_cmd(['install', "numpy<2"])
|
||||
subprocess.check_output(cmd , universal_newlines=True)
|
||||
|
||||
logging.info("[ComfyUI-Manager] 'numpy' dependency were fixed")
|
||||
except Exception as e:
|
||||
logging.error("[ComfyUI-Manager] Failed to restore numpy")
|
||||
logging.error(e)
|
||||
|
||||
# fix missing frontend
|
||||
try:
|
||||
# NOTE: package name in requirements is 'comfyui-frontend-package'
|
||||
@@ -540,3 +569,69 @@ def robust_readlines(fullpath):
|
||||
|
||||
print(f"[ComfyUI-Manager] Failed to recognize encoding for: {fullpath}")
|
||||
return []
|
||||
|
||||
|
||||
def restore_pip_snapshot(pips, options):
|
||||
non_url = []
|
||||
local_url = []
|
||||
non_local_url = []
|
||||
|
||||
for k, v in pips.items():
|
||||
# NOTE: skip torch related packages
|
||||
if k.startswith("torch==") or k.startswith("torchvision==") or k.startswith("torchaudio==") or k.startswith("nvidia-"):
|
||||
continue
|
||||
|
||||
if v == "":
|
||||
non_url.append(k)
|
||||
else:
|
||||
if v.startswith('file:'):
|
||||
local_url.append(v)
|
||||
else:
|
||||
non_local_url.append(v)
|
||||
|
||||
|
||||
# restore other pips
|
||||
failed = []
|
||||
if '--pip-non-url' in options:
|
||||
# try all at once
|
||||
res = 1
|
||||
try:
|
||||
res = subprocess.check_output(make_pip_cmd(['install'] + non_url))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# fallback
|
||||
if res != 0:
|
||||
for x in non_url:
|
||||
res = 1
|
||||
try:
|
||||
res = subprocess.check_output(make_pip_cmd(['install', '--no-deps', x]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if res != 0:
|
||||
failed.append(x)
|
||||
|
||||
if '--pip-non-local-url' in options:
|
||||
for x in non_local_url:
|
||||
res = 1
|
||||
try:
|
||||
res = subprocess.check_output(make_pip_cmd(['install', '--no-deps', x]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if res != 0:
|
||||
failed.append(x)
|
||||
|
||||
if '--pip-local-url' in options:
|
||||
for x in local_url:
|
||||
res = 1
|
||||
try:
|
||||
res = subprocess.check_output(make_pip_cmd(['install', '--no-deps', x]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if res != 0:
|
||||
failed.append(x)
|
||||
|
||||
print(f"Installation failed for pip packages: {failed}")
|
||||
@@ -2,6 +2,8 @@ import sys
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
from . import manager_util
|
||||
|
||||
|
||||
def security_check():
|
||||
print("[START] Security scan")
|
||||
@@ -66,18 +68,23 @@ https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situati
|
||||
"lolMiner": [os.path.join(comfyui_path, 'lolMiner')]
|
||||
}
|
||||
|
||||
installed_pips = subprocess.check_output([sys.executable, '-m', "pip", "freeze"], text=True)
|
||||
installed_pips = subprocess.check_output(manager_util.make_pip_cmd(["freeze"]), text=True)
|
||||
|
||||
detected = set()
|
||||
try:
|
||||
anthropic_info = subprocess.check_output([sys.executable, '-m', "pip", "show", "anthropic"], text=True, stderr=subprocess.DEVNULL)
|
||||
anthropic_reqs = [x for x in anthropic_info.split('\n') if x.startswith("Requires")][0].split(': ')[1]
|
||||
if "pycrypto" in anthropic_reqs:
|
||||
location = [x for x in anthropic_info.split('\n') if x.startswith("Location")][0].split(': ')[1]
|
||||
for fi in os.listdir(location):
|
||||
if fi.startswith("anthropic"):
|
||||
guide["ComfyUI_LLMVISION"] = f"\n0.Remove {os.path.join(location, fi)}" + guide["ComfyUI_LLMVISION"]
|
||||
detected.add("ComfyUI_LLMVISION")
|
||||
anthropic_info = subprocess.check_output(manager_util.make_pip_cmd(["show", "anthropic"]), text=True, stderr=subprocess.DEVNULL)
|
||||
requires_lines = [x for x in anthropic_info.split('\n') if x.startswith("Requires")]
|
||||
if requires_lines:
|
||||
anthropic_reqs = requires_lines[0].split(": ", 1)[1]
|
||||
if "pycrypto" in anthropic_reqs:
|
||||
location_lines = [x for x in anthropic_info.split('\n') if x.startswith("Location")]
|
||||
if location_lines:
|
||||
location = location_lines[0].split(": ", 1)[1]
|
||||
for fi in os.listdir(location):
|
||||
if fi.startswith("anthropic"):
|
||||
guide["ComfyUI_LLMVISION"] = (f"\n0.Remove {os.path.join(location, fi)}" + guide["ComfyUI_LLMVISION"])
|
||||
detected.add("ComfyUI_LLMVISION")
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Robust timestamp utilities with datetime fallback.
|
||||
|
||||
Some environments (especially Mac) have issues with the datetime module
|
||||
due to local file name conflicts or Homebrew Python module path issues.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time as time_module
|
||||
import uuid
|
||||
|
||||
_datetime_available = None
|
||||
_dt_datetime = None
|
||||
|
||||
|
||||
def _init_datetime():
|
||||
"""Initialize datetime availability check (lazy, once)."""
|
||||
global _datetime_available, _dt_datetime
|
||||
if _datetime_available is not None:
|
||||
return
|
||||
|
||||
try:
|
||||
import datetime as dt
|
||||
if hasattr(dt, 'datetime'):
|
||||
from datetime import datetime as dt_datetime
|
||||
_dt_datetime = dt_datetime
|
||||
_datetime_available = True
|
||||
return
|
||||
except Exception as e:
|
||||
logging.debug(f"[ComfyUI-Manager] datetime import failed: {e}")
|
||||
|
||||
_datetime_available = False
|
||||
logging.warning("[ComfyUI-Manager] datetime unavailable, using time module fallback")
|
||||
|
||||
|
||||
def current_timestamp() -> str:
|
||||
"""
|
||||
Get current timestamp for logging.
|
||||
Format: YYYY-MM-DD HH:MM:SS.mmm (or Unix timestamp if fallback)
|
||||
"""
|
||||
_init_datetime()
|
||||
if _datetime_available:
|
||||
return _dt_datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
||||
return str(time_module.time()).split('.')[0]
|
||||
|
||||
|
||||
def get_timestamp_for_filename() -> str:
|
||||
"""
|
||||
Get timestamp suitable for filenames.
|
||||
Format: YYYYMMDD_HHMMSS
|
||||
"""
|
||||
_init_datetime()
|
||||
if _datetime_available:
|
||||
return _dt_datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
return time_module.strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
|
||||
def get_timestamp_for_path() -> str:
|
||||
"""
|
||||
Get timestamp for path/directory names.
|
||||
Format: YYYY-MM-DD_HH-MM-SS
|
||||
"""
|
||||
_init_datetime()
|
||||
if _datetime_available:
|
||||
return _dt_datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
|
||||
return time_module.strftime('%Y-%m-%d_%H-%M-%S')
|
||||
|
||||
|
||||
def get_backup_branch_name(repo=None) -> str:
|
||||
"""
|
||||
Get backup branch name with current timestamp.
|
||||
Format: backup_YYYYMMDD_HHMMSS (or backup_YYYYMMDD_HHMMSS_N if exists)
|
||||
|
||||
Args:
|
||||
repo: Optional git.Repo object. If provided, checks for name collisions
|
||||
and adds sequential suffix if needed.
|
||||
|
||||
Returns:
|
||||
Unique backup branch name.
|
||||
"""
|
||||
base_name = f'backup_{get_timestamp_for_filename()}'
|
||||
|
||||
if repo is None:
|
||||
return base_name
|
||||
|
||||
# Check if branch exists
|
||||
try:
|
||||
existing_branches = {b.name for b in repo.heads}
|
||||
except Exception:
|
||||
return base_name
|
||||
|
||||
if base_name not in existing_branches:
|
||||
return base_name
|
||||
|
||||
# Add sequential suffix
|
||||
for i in range(1, 100):
|
||||
new_name = f'{base_name}_{i}'
|
||||
if new_name not in existing_branches:
|
||||
return new_name
|
||||
|
||||
# Ultimate fallback: use UUID (very unlikely to reach here)
|
||||
return f'{base_name}_{uuid.uuid4().hex[:6]}'
|
||||
|
||||
|
||||
def get_now():
|
||||
"""
|
||||
Get current datetime object.
|
||||
Returns datetime.now() if available, otherwise a FakeDatetime object
|
||||
that supports basic operations (timestamp(), strftime()).
|
||||
"""
|
||||
_init_datetime()
|
||||
if _datetime_available:
|
||||
return _dt_datetime.now()
|
||||
|
||||
# Fallback: return object with basic datetime-like interface
|
||||
t = time_module.localtime()
|
||||
|
||||
class FakeDatetime:
|
||||
def timestamp(self):
|
||||
return time_module.time()
|
||||
|
||||
def strftime(self, fmt):
|
||||
return time_module.strftime(fmt, t)
|
||||
|
||||
def isoformat(self):
|
||||
return time_module.strftime('%Y-%m-%dT%H:%M:%S', t)
|
||||
|
||||
return FakeDatetime()
|
||||
|
||||
|
||||
def get_unix_timestamp() -> float:
|
||||
"""Get current Unix timestamp."""
|
||||
_init_datetime()
|
||||
if _datetime_available:
|
||||
return _dt_datetime.now().timestamp()
|
||||
return time_module.time()
|
||||
Executable → Regular
+10575
-724
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,7 @@ datamodel-codegen \
|
||||
--use-subclass-enum \
|
||||
--field-constraints \
|
||||
--strict-types bytes \
|
||||
--use-double-quotes \
|
||||
--input openapi.yaml \
|
||||
--output comfyui_manager/data_models/generated_models.py \
|
||||
--output-model-type pydantic_v2.BaseModel
|
||||
|
||||
@@ -30,9 +30,15 @@ from .generated_models import (
|
||||
InstalledModelInfo,
|
||||
ComfyUIVersionInfo,
|
||||
|
||||
# Import Fail Info Models
|
||||
ImportFailInfoBulkRequest,
|
||||
ImportFailInfoBulkResponse,
|
||||
ImportFailInfoItem,
|
||||
ImportFailInfoItem1,
|
||||
|
||||
# Other models
|
||||
Kind,
|
||||
StatusStr,
|
||||
OperationType,
|
||||
OperationResult,
|
||||
ManagerPackInfo,
|
||||
ManagerPackInstalled,
|
||||
SelectedVersion,
|
||||
@@ -49,6 +55,9 @@ from .generated_models import (
|
||||
UninstallPackParams,
|
||||
DisablePackParams,
|
||||
EnablePackParams,
|
||||
UpdateAllQueryParams,
|
||||
UpdateComfyUIQueryParams,
|
||||
ComfyUISwitchVersionQueryParams,
|
||||
QueueStatus,
|
||||
ManagerMappings,
|
||||
ModelMetadata,
|
||||
@@ -59,8 +68,8 @@ from .generated_models import (
|
||||
HistoryResponse,
|
||||
HistoryListResponse,
|
||||
InstallType,
|
||||
OperationType,
|
||||
Result,
|
||||
SecurityLevel,
|
||||
RiskLevel,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -85,9 +94,15 @@ __all__ = [
|
||||
"InstalledModelInfo",
|
||||
"ComfyUIVersionInfo",
|
||||
|
||||
# Import Fail Info Models
|
||||
"ImportFailInfoBulkRequest",
|
||||
"ImportFailInfoBulkResponse",
|
||||
"ImportFailInfoItem",
|
||||
"ImportFailInfoItem1",
|
||||
|
||||
# Other models
|
||||
"Kind",
|
||||
"StatusStr",
|
||||
"OperationType",
|
||||
"OperationResult",
|
||||
"ManagerPackInfo",
|
||||
"ManagerPackInstalled",
|
||||
"SelectedVersion",
|
||||
@@ -104,6 +119,9 @@ __all__ = [
|
||||
"UninstallPackParams",
|
||||
"DisablePackParams",
|
||||
"EnablePackParams",
|
||||
"UpdateAllQueryParams",
|
||||
"UpdateComfyUIQueryParams",
|
||||
"ComfyUISwitchVersionQueryParams",
|
||||
"QueueStatus",
|
||||
"ManagerMappings",
|
||||
"ModelMetadata",
|
||||
@@ -114,6 +132,6 @@ __all__ = [
|
||||
"HistoryResponse",
|
||||
"HistoryListResponse",
|
||||
"InstallType",
|
||||
"OperationType",
|
||||
"Result",
|
||||
"SecurityLevel",
|
||||
"RiskLevel",
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: openapi.yaml
|
||||
# timestamp: 2025-06-14T01:44:21+00:00
|
||||
# timestamp: 2025-07-31T04:52:26+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -11,252 +11,298 @@ from typing import Any, Dict, List, Optional, Union
|
||||
from pydantic import BaseModel, Field, RootModel
|
||||
|
||||
|
||||
class Kind(str, Enum):
|
||||
install = 'install'
|
||||
uninstall = 'uninstall'
|
||||
update = 'update'
|
||||
update_all = 'update-all'
|
||||
update_comfyui = 'update-comfyui'
|
||||
fix = 'fix'
|
||||
disable = 'disable'
|
||||
enable = 'enable'
|
||||
install_model = 'install-model'
|
||||
class OperationType(str, Enum):
|
||||
install = "install"
|
||||
uninstall = "uninstall"
|
||||
update = "update"
|
||||
update_comfyui = "update-comfyui"
|
||||
fix = "fix"
|
||||
disable = "disable"
|
||||
enable = "enable"
|
||||
install_model = "install-model"
|
||||
|
||||
|
||||
class StatusStr(str, Enum):
|
||||
success = 'success'
|
||||
error = 'error'
|
||||
skip = 'skip'
|
||||
class OperationResult(str, Enum):
|
||||
success = "success"
|
||||
failed = "failed"
|
||||
skipped = "skipped"
|
||||
error = "error"
|
||||
skip = "skip"
|
||||
|
||||
|
||||
class TaskExecutionStatus(BaseModel):
|
||||
status_str: StatusStr = Field(..., description='Overall task execution status')
|
||||
completed: bool = Field(..., description='Whether the task completed')
|
||||
messages: List[str] = Field(..., description='Additional status messages')
|
||||
status_str: OperationResult
|
||||
completed: bool = Field(..., description="Whether the task completed")
|
||||
messages: List[str] = Field(..., description="Additional status messages")
|
||||
|
||||
|
||||
class ManagerMessageName(str, Enum):
|
||||
cm_task_completed = 'cm-task-completed'
|
||||
cm_task_started = 'cm-task-started'
|
||||
cm_queue_status = 'cm-queue-status'
|
||||
cm_task_completed = "cm-task-completed"
|
||||
cm_task_started = "cm-task-started"
|
||||
cm_queue_status = "cm-queue-status"
|
||||
|
||||
|
||||
class ManagerPackInfo(BaseModel):
|
||||
id: str = Field(
|
||||
...,
|
||||
description='Either github-author/github-repo or name of pack from the registry',
|
||||
description="Either github-author/github-repo or name of pack from the registry",
|
||||
)
|
||||
version: str = Field(..., description='Semantic version or Git commit hash')
|
||||
ui_id: Optional[str] = Field(None, description='Task ID - generated internally')
|
||||
version: str = Field(..., description="Semantic version or Git commit hash")
|
||||
ui_id: Optional[str] = Field(None, description="Task ID - generated internally")
|
||||
|
||||
|
||||
class ManagerPackInstalled(BaseModel):
|
||||
ver: str = Field(
|
||||
...,
|
||||
description='The version of the pack that is installed (Git commit hash or semantic version)',
|
||||
description="The version of the pack that is installed (Git commit hash or semantic version)",
|
||||
)
|
||||
cnr_id: Optional[str] = Field(
|
||||
None, description='The name of the pack if installed from the registry'
|
||||
None, description="The name of the pack if installed from the registry"
|
||||
)
|
||||
aux_id: Optional[str] = Field(
|
||||
None,
|
||||
description='The name of the pack if installed from github (author/repo-name format)',
|
||||
description="The name of the pack if installed from github (author/repo-name format)",
|
||||
)
|
||||
enabled: bool = Field(..., description='Whether the pack is enabled')
|
||||
enabled: bool = Field(..., description="Whether the pack is enabled")
|
||||
|
||||
|
||||
class SelectedVersion(str, Enum):
|
||||
latest = 'latest'
|
||||
nightly = 'nightly'
|
||||
latest = "latest"
|
||||
nightly = "nightly"
|
||||
|
||||
|
||||
class ManagerChannel(str, Enum):
|
||||
default = 'default'
|
||||
recent = 'recent'
|
||||
legacy = 'legacy'
|
||||
forked = 'forked'
|
||||
dev = 'dev'
|
||||
tutorial = 'tutorial'
|
||||
default = "default"
|
||||
recent = "recent"
|
||||
legacy = "legacy"
|
||||
forked = "forked"
|
||||
dev = "dev"
|
||||
tutorial = "tutorial"
|
||||
|
||||
|
||||
class ManagerDatabaseSource(str, Enum):
|
||||
remote = 'remote'
|
||||
local = 'local'
|
||||
cache = 'cache'
|
||||
remote = "remote"
|
||||
local = "local"
|
||||
cache = "cache"
|
||||
|
||||
|
||||
class ManagerPackState(str, Enum):
|
||||
installed = 'installed'
|
||||
disabled = 'disabled'
|
||||
not_installed = 'not_installed'
|
||||
import_failed = 'import_failed'
|
||||
needs_update = 'needs_update'
|
||||
installed = "installed"
|
||||
disabled = "disabled"
|
||||
not_installed = "not_installed"
|
||||
import_failed = "import_failed"
|
||||
needs_update = "needs_update"
|
||||
|
||||
|
||||
class ManagerPackInstallType(str, Enum):
|
||||
git_clone = 'git-clone'
|
||||
copy = 'copy'
|
||||
cnr = 'cnr'
|
||||
git_clone = "git-clone"
|
||||
copy = "copy"
|
||||
cnr = "cnr"
|
||||
|
||||
|
||||
class SecurityLevel(str, Enum):
|
||||
strong = "strong"
|
||||
normal = "normal"
|
||||
normal_ = "normal-"
|
||||
weak = "weak"
|
||||
|
||||
|
||||
class RiskLevel(str, Enum):
|
||||
block = "block"
|
||||
high_ = "high+"
|
||||
high = "high"
|
||||
middle_ = "middle+"
|
||||
middle = "middle"
|
||||
|
||||
|
||||
class UpdateState(Enum):
|
||||
false = 'false'
|
||||
true = 'true'
|
||||
false = "false"
|
||||
true = "true"
|
||||
|
||||
|
||||
class ManagerPack(ManagerPackInfo):
|
||||
author: Optional[str] = Field(
|
||||
None, description="Pack author name or 'Unclaimed' if added via GitHub crawl"
|
||||
)
|
||||
files: Optional[List[str]] = Field(None, description='Files included in the pack')
|
||||
reference: Optional[str] = Field(
|
||||
None, description='The type of installation reference'
|
||||
files: Optional[List[str]] = Field(
|
||||
None,
|
||||
description="Repository URLs for installation (typically contains one GitHub URL)",
|
||||
)
|
||||
title: Optional[str] = Field(None, description='The display name of the pack')
|
||||
reference: Optional[str] = Field(
|
||||
None, description="The type of installation reference"
|
||||
)
|
||||
title: Optional[str] = Field(None, description="The display name of the pack")
|
||||
cnr_latest: Optional[SelectedVersion] = None
|
||||
repository: Optional[str] = Field(None, description='GitHub repository URL')
|
||||
repository: Optional[str] = Field(None, description="GitHub repository URL")
|
||||
state: Optional[ManagerPackState] = None
|
||||
update_state: Optional[UpdateState] = Field(
|
||||
None, alias='update-state', description='Update availability status'
|
||||
None, alias="update-state", description="Update availability status"
|
||||
)
|
||||
stars: Optional[int] = Field(None, description='GitHub stars count')
|
||||
last_update: Optional[datetime] = Field(None, description='Last update timestamp')
|
||||
health: Optional[str] = Field(None, description='Health status of the pack')
|
||||
description: Optional[str] = Field(None, description='Pack description')
|
||||
trust: Optional[bool] = Field(None, description='Whether the pack is trusted')
|
||||
stars: Optional[int] = Field(None, description="GitHub stars count")
|
||||
last_update: Optional[datetime] = Field(None, description="Last update timestamp")
|
||||
health: Optional[str] = Field(None, description="Health status of the pack")
|
||||
description: Optional[str] = Field(None, description="Pack description")
|
||||
trust: Optional[bool] = Field(None, description="Whether the pack is trusted")
|
||||
install_type: Optional[ManagerPackInstallType] = None
|
||||
|
||||
|
||||
class InstallPackParams(ManagerPackInfo):
|
||||
selected_version: Union[str, SelectedVersion] = Field(
|
||||
..., description='Semantic version, Git commit hash, latest, or nightly'
|
||||
..., description="Semantic version, Git commit hash, latest, or nightly"
|
||||
)
|
||||
repository: Optional[str] = Field(
|
||||
None,
|
||||
description='GitHub repository URL (required if selected_version is nightly)',
|
||||
description="GitHub repository URL (required if selected_version is nightly)",
|
||||
)
|
||||
pip: Optional[List[str]] = Field(None, description='PyPi dependency names')
|
||||
pip: Optional[List[str]] = Field(None, description="PyPi dependency names")
|
||||
mode: ManagerDatabaseSource
|
||||
channel: ManagerChannel
|
||||
skip_post_install: Optional[bool] = Field(
|
||||
None, description='Whether to skip post-installation steps'
|
||||
None, description="Whether to skip post-installation steps"
|
||||
)
|
||||
|
||||
|
||||
class UpdateAllPacksParams(BaseModel):
|
||||
mode: Optional[ManagerDatabaseSource] = None
|
||||
ui_id: Optional[str] = Field(None, description='Task ID - generated internally')
|
||||
ui_id: Optional[str] = Field(None, description="Task ID - generated internally")
|
||||
|
||||
|
||||
class UpdatePackParams(BaseModel):
|
||||
node_name: str = Field(..., description='Name of the node package to update')
|
||||
node_name: str = Field(..., description="Name of the node package to update")
|
||||
node_ver: Optional[str] = Field(
|
||||
None, description='Current version of the node package'
|
||||
None, description="Current version of the node package"
|
||||
)
|
||||
|
||||
|
||||
class UpdateComfyUIParams(BaseModel):
|
||||
is_stable: Optional[bool] = Field(
|
||||
True,
|
||||
description='Whether to update to stable version (true) or nightly (false)',
|
||||
description="Whether to update to stable version (true) or nightly (false)",
|
||||
)
|
||||
target_version: Optional[str] = Field(
|
||||
None,
|
||||
description='Specific version to switch to (for version switching operations)',
|
||||
description="Specific version to switch to (for version switching operations)",
|
||||
)
|
||||
|
||||
|
||||
class FixPackParams(BaseModel):
|
||||
node_name: str = Field(..., description='Name of the node package to fix')
|
||||
node_ver: str = Field(..., description='Version of the node package')
|
||||
node_name: str = Field(..., description="Name of the node package to fix")
|
||||
node_ver: str = Field(..., description="Version of the node package")
|
||||
|
||||
|
||||
class UninstallPackParams(BaseModel):
|
||||
node_name: str = Field(..., description='Name of the node package to uninstall')
|
||||
node_name: str = Field(..., description="Name of the node package to uninstall")
|
||||
is_unknown: Optional[bool] = Field(
|
||||
False, description='Whether this is an unknown/unregistered package'
|
||||
False, description="Whether this is an unknown/unregistered package"
|
||||
)
|
||||
|
||||
|
||||
class DisablePackParams(BaseModel):
|
||||
node_name: str = Field(..., description='Name of the node package to disable')
|
||||
node_name: str = Field(..., description="Name of the node package to disable")
|
||||
is_unknown: Optional[bool] = Field(
|
||||
False, description='Whether this is an unknown/unregistered package'
|
||||
False, description="Whether this is an unknown/unregistered package"
|
||||
)
|
||||
|
||||
|
||||
class EnablePackParams(BaseModel):
|
||||
cnr_id: str = Field(
|
||||
..., description='ComfyUI Node Registry ID of the package to enable'
|
||||
..., description="ComfyUI Node Registry ID of the package to enable"
|
||||
)
|
||||
|
||||
|
||||
class UpdateAllQueryParams(BaseModel):
|
||||
client_id: str = Field(
|
||||
..., description="Client identifier that initiated the request"
|
||||
)
|
||||
ui_id: str = Field(..., description="Base UI identifier for task tracking")
|
||||
mode: Optional[ManagerDatabaseSource] = None
|
||||
|
||||
|
||||
class UpdateComfyUIQueryParams(BaseModel):
|
||||
client_id: str = Field(
|
||||
..., description="Client identifier that initiated the request"
|
||||
)
|
||||
ui_id: str = Field(..., description="UI identifier for task tracking")
|
||||
stable: Optional[bool] = Field(
|
||||
True,
|
||||
description="Whether to update to stable version (true) or nightly (false)",
|
||||
)
|
||||
|
||||
|
||||
class ComfyUISwitchVersionQueryParams(BaseModel):
|
||||
ver: str = Field(..., description="Version to switch to")
|
||||
client_id: str = Field(
|
||||
..., description="Client identifier that initiated the request"
|
||||
)
|
||||
ui_id: str = Field(..., description="UI identifier for task tracking")
|
||||
|
||||
|
||||
class QueueStatus(BaseModel):
|
||||
total_count: int = Field(
|
||||
..., description='Total number of tasks (pending + running)'
|
||||
..., description="Total number of tasks (pending + running)"
|
||||
)
|
||||
done_count: int = Field(..., description='Number of completed tasks')
|
||||
in_progress_count: int = Field(..., description='Number of tasks currently running')
|
||||
done_count: int = Field(..., description="Number of completed tasks")
|
||||
in_progress_count: int = Field(..., description="Number of tasks currently running")
|
||||
pending_count: Optional[int] = Field(
|
||||
None, description='Number of tasks waiting to be executed'
|
||||
None, description="Number of tasks waiting to be executed"
|
||||
)
|
||||
is_processing: bool = Field(..., description='Whether the task worker is active')
|
||||
is_processing: bool = Field(..., description="Whether the task worker is active")
|
||||
client_id: Optional[str] = Field(
|
||||
None, description='Client ID (when filtered by client)'
|
||||
None, description="Client ID (when filtered by client)"
|
||||
)
|
||||
|
||||
|
||||
class ManagerMappings1(BaseModel):
|
||||
title_aux: Optional[str] = Field(None, description='The display name of the pack')
|
||||
title_aux: Optional[str] = Field(None, description="The display name of the pack")
|
||||
|
||||
|
||||
class ManagerMappings(
|
||||
RootModel[Optional[Dict[str, List[Union[List[str], ManagerMappings1]]]]]
|
||||
):
|
||||
root: Optional[Dict[str, List[Union[List[str], ManagerMappings1]]]] = Field(
|
||||
None, description='Tuple of [node_names, metadata]'
|
||||
None, description="Tuple of [node_names, metadata]"
|
||||
)
|
||||
|
||||
|
||||
class ModelMetadata(BaseModel):
|
||||
name: str = Field(..., description='Name of the model')
|
||||
type: str = Field(..., description='Type of model')
|
||||
base: Optional[str] = Field(None, description='Base model type')
|
||||
save_path: Optional[str] = Field(None, description='Path for saving the model')
|
||||
url: str = Field(..., description='Download URL')
|
||||
filename: str = Field(..., description='Target filename')
|
||||
ui_id: Optional[str] = Field(None, description='ID for UI reference')
|
||||
name: str = Field(..., description="Name of the model")
|
||||
type: str = Field(..., description="Type of model")
|
||||
base: Optional[str] = Field(None, description="Base model type")
|
||||
save_path: Optional[str] = Field(None, description="Path for saving the model")
|
||||
url: str = Field(..., description="Download URL")
|
||||
filename: str = Field(..., description="Target filename")
|
||||
ui_id: Optional[str] = Field(None, description="ID for UI reference")
|
||||
|
||||
|
||||
class InstallType(str, Enum):
|
||||
git = 'git'
|
||||
copy = 'copy'
|
||||
pip = 'pip'
|
||||
git = "git"
|
||||
copy = "copy"
|
||||
pip = "pip"
|
||||
|
||||
|
||||
class NodePackageMetadata(BaseModel):
|
||||
title: Optional[str] = Field(None, description='Display name of the node package')
|
||||
name: Optional[str] = Field(None, description='Repository/package name')
|
||||
files: Optional[List[str]] = Field(None, description='Source URLs for the package')
|
||||
title: Optional[str] = Field(None, description="Display name of the node package")
|
||||
name: Optional[str] = Field(None, description="Repository/package name")
|
||||
files: Optional[List[str]] = Field(None, description="Source URLs for the package")
|
||||
description: Optional[str] = Field(
|
||||
None, description='Description of the node package functionality'
|
||||
None, description="Description of the node package functionality"
|
||||
)
|
||||
install_type: Optional[InstallType] = Field(None, description='Installation method')
|
||||
version: Optional[str] = Field(None, description='Version identifier')
|
||||
install_type: Optional[InstallType] = Field(None, description="Installation method")
|
||||
version: Optional[str] = Field(None, description="Version identifier")
|
||||
id: Optional[str] = Field(
|
||||
None, description='Unique identifier for the node package'
|
||||
None, description="Unique identifier for the node package"
|
||||
)
|
||||
ui_id: Optional[str] = Field(None, description='ID for UI reference')
|
||||
channel: Optional[str] = Field(None, description='Source channel')
|
||||
mode: Optional[str] = Field(None, description='Source mode')
|
||||
ui_id: Optional[str] = Field(None, description="ID for UI reference")
|
||||
channel: Optional[str] = Field(None, description="Source channel")
|
||||
mode: Optional[str] = Field(None, description="Source mode")
|
||||
|
||||
|
||||
class SnapshotItem(RootModel[str]):
|
||||
root: str = Field(..., description='Name of the snapshot')
|
||||
root: str = Field(..., description="Name of the snapshot")
|
||||
|
||||
|
||||
class Error(BaseModel):
|
||||
error: str = Field(..., description='Error message')
|
||||
error: str = Field(..., description="Error message")
|
||||
|
||||
|
||||
class InstalledPacksResponse(RootModel[Optional[Dict[str, ManagerPackInstalled]]]):
|
||||
@@ -265,142 +311,171 @@ class InstalledPacksResponse(RootModel[Optional[Dict[str, ManagerPackInstalled]]
|
||||
|
||||
class HistoryListResponse(BaseModel):
|
||||
ids: Optional[List[str]] = Field(
|
||||
None, description='List of available batch history IDs'
|
||||
None, description="List of available batch history IDs"
|
||||
)
|
||||
|
||||
|
||||
class InstalledNodeInfo(BaseModel):
|
||||
name: str = Field(..., description='Node package name')
|
||||
version: str = Field(..., description='Installed version')
|
||||
repository_url: Optional[str] = Field(None, description='Git repository URL')
|
||||
name: str = Field(..., description="Node package name")
|
||||
version: str = Field(..., description="Installed version")
|
||||
repository_url: Optional[str] = Field(None, description="Git repository URL")
|
||||
install_method: str = Field(
|
||||
..., description='Installation method (cnr, git, pip, etc.)'
|
||||
..., description="Installation method (cnr, git, pip, etc.)"
|
||||
)
|
||||
enabled: Optional[bool] = Field(
|
||||
True, description='Whether the node is currently enabled'
|
||||
True, description="Whether the node is currently enabled"
|
||||
)
|
||||
install_date: Optional[datetime] = Field(
|
||||
None, description='ISO timestamp of installation'
|
||||
None, description="ISO timestamp of installation"
|
||||
)
|
||||
|
||||
|
||||
class InstalledModelInfo(BaseModel):
|
||||
name: str = Field(..., description='Model filename')
|
||||
path: str = Field(..., description='Full path to model file')
|
||||
type: str = Field(..., description='Model type (checkpoint, lora, vae, etc.)')
|
||||
size_bytes: Optional[int] = Field(None, description='File size in bytes', ge=0)
|
||||
hash: Optional[str] = Field(None, description='Model file hash for verification')
|
||||
name: str = Field(..., description="Model filename")
|
||||
path: str = Field(..., description="Full path to model file")
|
||||
type: str = Field(..., description="Model type (checkpoint, lora, vae, etc.)")
|
||||
size_bytes: Optional[int] = Field(None, description="File size in bytes", ge=0)
|
||||
hash: Optional[str] = Field(None, description="Model file hash for verification")
|
||||
install_date: Optional[datetime] = Field(
|
||||
None, description='ISO timestamp when added'
|
||||
None, description="ISO timestamp when added"
|
||||
)
|
||||
|
||||
|
||||
class ComfyUIVersionInfo(BaseModel):
|
||||
version: str = Field(..., description='ComfyUI version string')
|
||||
commit_hash: Optional[str] = Field(None, description='Git commit hash')
|
||||
branch: Optional[str] = Field(None, description='Git branch name')
|
||||
version: str = Field(..., description="ComfyUI version string")
|
||||
commit_hash: Optional[str] = Field(None, description="Git commit hash")
|
||||
branch: Optional[str] = Field(None, description="Git branch name")
|
||||
is_stable: Optional[bool] = Field(
|
||||
False, description='Whether this is a stable release'
|
||||
False, description="Whether this is a stable release"
|
||||
)
|
||||
last_updated: Optional[datetime] = Field(
|
||||
None, description='ISO timestamp of last update'
|
||||
None, description="ISO timestamp of last update"
|
||||
)
|
||||
|
||||
|
||||
class OperationType(str, Enum):
|
||||
install = 'install'
|
||||
update = 'update'
|
||||
uninstall = 'uninstall'
|
||||
fix = 'fix'
|
||||
disable = 'disable'
|
||||
enable = 'enable'
|
||||
install_model = 'install-model'
|
||||
|
||||
|
||||
class Result(str, Enum):
|
||||
success = 'success'
|
||||
failed = 'failed'
|
||||
skipped = 'skipped'
|
||||
|
||||
|
||||
class BatchOperation(BaseModel):
|
||||
operation_id: str = Field(..., description='Unique operation identifier')
|
||||
operation_type: OperationType = Field(..., description='Type of operation')
|
||||
operation_id: str = Field(..., description="Unique operation identifier")
|
||||
operation_type: OperationType
|
||||
target: str = Field(
|
||||
..., description='Target of the operation (node name, model name, etc.)'
|
||||
..., description="Target of the operation (node name, model name, etc.)"
|
||||
)
|
||||
target_version: Optional[str] = Field(
|
||||
None, description='Target version for the operation'
|
||||
None, description="Target version for the operation"
|
||||
)
|
||||
result: Result = Field(..., description='Operation result')
|
||||
result: OperationResult
|
||||
error_message: Optional[str] = Field(
|
||||
None, description='Error message if operation failed'
|
||||
None, description="Error message if operation failed"
|
||||
)
|
||||
start_time: datetime = Field(
|
||||
..., description='ISO timestamp when operation started'
|
||||
..., description="ISO timestamp when operation started"
|
||||
)
|
||||
end_time: Optional[datetime] = Field(
|
||||
None, description='ISO timestamp when operation completed'
|
||||
None, description="ISO timestamp when operation completed"
|
||||
)
|
||||
client_id: Optional[str] = Field(
|
||||
None, description='Client that initiated the operation'
|
||||
None, description="Client that initiated the operation"
|
||||
)
|
||||
|
||||
|
||||
class ComfyUISystemState(BaseModel):
|
||||
snapshot_time: datetime = Field(
|
||||
..., description='ISO timestamp when snapshot was taken'
|
||||
..., description="ISO timestamp when snapshot was taken"
|
||||
)
|
||||
comfyui_version: ComfyUIVersionInfo
|
||||
frontend_version: Optional[str] = Field(
|
||||
None, description='ComfyUI frontend version if available'
|
||||
None, description="ComfyUI frontend version if available"
|
||||
)
|
||||
python_version: str = Field(..., description='Python interpreter version')
|
||||
python_version: str = Field(..., description="Python interpreter version")
|
||||
platform_info: str = Field(
|
||||
..., description='Operating system and platform information'
|
||||
..., description="Operating system and platform information"
|
||||
)
|
||||
installed_nodes: Optional[Dict[str, InstalledNodeInfo]] = Field(
|
||||
None, description='Map of installed node packages by name'
|
||||
None, description="Map of installed node packages by name"
|
||||
)
|
||||
installed_models: Optional[Dict[str, InstalledModelInfo]] = Field(
|
||||
None, description='Map of installed models by name'
|
||||
None, description="Map of installed models by name"
|
||||
)
|
||||
manager_config: Optional[Dict[str, Any]] = Field(
|
||||
None, description='ComfyUI Manager configuration settings'
|
||||
None, description="ComfyUI Manager configuration settings"
|
||||
)
|
||||
comfyui_root_path: Optional[str] = Field(
|
||||
None, description="ComfyUI root installation directory"
|
||||
)
|
||||
model_paths: Optional[Dict[str, List[str]]] = Field(
|
||||
None, description="Map of model types to their configured paths"
|
||||
)
|
||||
manager_version: Optional[str] = Field(None, description="ComfyUI Manager version")
|
||||
security_level: Optional[SecurityLevel] = None
|
||||
network_mode: Optional[str] = Field(
|
||||
None, description="Network mode (online, offline, private)"
|
||||
)
|
||||
cli_args: Optional[Dict[str, Any]] = Field(
|
||||
None, description="Selected ComfyUI CLI arguments"
|
||||
)
|
||||
custom_nodes_count: Optional[int] = Field(
|
||||
None, description="Total number of custom node packages", ge=0
|
||||
)
|
||||
failed_imports: Optional[List[str]] = Field(
|
||||
None, description="List of custom nodes that failed to import"
|
||||
)
|
||||
pip_packages: Optional[Dict[str, str]] = Field(
|
||||
None, description="Map of installed pip packages to their versions"
|
||||
)
|
||||
embedded_python: Optional[bool] = Field(
|
||||
None,
|
||||
description="Whether ComfyUI is running from an embedded Python distribution",
|
||||
)
|
||||
|
||||
|
||||
class BatchExecutionRecord(BaseModel):
|
||||
batch_id: str = Field(..., description='Unique batch identifier')
|
||||
start_time: datetime = Field(..., description='ISO timestamp when batch started')
|
||||
batch_id: str = Field(..., description="Unique batch identifier")
|
||||
start_time: datetime = Field(..., description="ISO timestamp when batch started")
|
||||
end_time: Optional[datetime] = Field(
|
||||
None, description='ISO timestamp when batch completed'
|
||||
None, description="ISO timestamp when batch completed"
|
||||
)
|
||||
state_before: ComfyUISystemState
|
||||
state_after: Optional[ComfyUISystemState] = Field(
|
||||
None, description='System state after batch execution'
|
||||
None, description="System state after batch execution"
|
||||
)
|
||||
operations: Optional[List[BatchOperation]] = Field(
|
||||
None, description='List of operations performed in this batch'
|
||||
None, description="List of operations performed in this batch"
|
||||
)
|
||||
total_operations: Optional[int] = Field(
|
||||
0, description='Total number of operations in batch', ge=0
|
||||
0, description="Total number of operations in batch", ge=0
|
||||
)
|
||||
successful_operations: Optional[int] = Field(
|
||||
0, description='Number of successful operations', ge=0
|
||||
0, description="Number of successful operations", ge=0
|
||||
)
|
||||
failed_operations: Optional[int] = Field(
|
||||
0, description='Number of failed operations', ge=0
|
||||
0, description="Number of failed operations", ge=0
|
||||
)
|
||||
skipped_operations: Optional[int] = Field(
|
||||
0, description='Number of skipped operations', ge=0
|
||||
0, description="Number of skipped operations", ge=0
|
||||
)
|
||||
|
||||
|
||||
class ImportFailInfoBulkRequest(BaseModel):
|
||||
cnr_ids: Optional[List[str]] = Field(
|
||||
None, description="A list of CNR IDs to check."
|
||||
)
|
||||
urls: Optional[List[str]] = Field(
|
||||
None, description="A list of repository URLs to check."
|
||||
)
|
||||
|
||||
|
||||
class ImportFailInfoItem1(BaseModel):
|
||||
error: Optional[str] = None
|
||||
traceback: Optional[str] = None
|
||||
|
||||
|
||||
class ImportFailInfoItem(RootModel[Optional[ImportFailInfoItem1]]):
|
||||
root: Optional[ImportFailInfoItem1]
|
||||
|
||||
|
||||
class QueueTaskItem(BaseModel):
|
||||
ui_id: str = Field(..., description='Unique identifier for the task')
|
||||
client_id: str = Field(..., description='Client identifier that initiated the task')
|
||||
kind: Kind = Field(..., description='Type of task being performed')
|
||||
ui_id: str = Field(..., description="Unique identifier for the task")
|
||||
client_id: str = Field(..., description="Client identifier that initiated the task")
|
||||
kind: OperationType
|
||||
params: Union[
|
||||
InstallPackParams,
|
||||
UpdatePackParams,
|
||||
@@ -415,50 +490,56 @@ class QueueTaskItem(BaseModel):
|
||||
|
||||
|
||||
class TaskHistoryItem(BaseModel):
|
||||
ui_id: str = Field(..., description='Unique identifier for the task')
|
||||
client_id: str = Field(..., description='Client identifier that initiated the task')
|
||||
kind: str = Field(..., description='Type of task that was performed')
|
||||
timestamp: datetime = Field(..., description='ISO timestamp when task completed')
|
||||
result: str = Field(..., description='Task result message or details')
|
||||
ui_id: str = Field(..., description="Unique identifier for the task")
|
||||
client_id: str = Field(..., description="Client identifier that initiated the task")
|
||||
kind: str = Field(..., description="Type of task that was performed")
|
||||
timestamp: datetime = Field(..., description="ISO timestamp when task completed")
|
||||
result: str = Field(..., description="Task result message or details")
|
||||
status: Optional[TaskExecutionStatus] = None
|
||||
batch_id: Optional[str] = Field(
|
||||
None, description="ID of the batch this task belongs to"
|
||||
)
|
||||
end_time: Optional[datetime] = Field(
|
||||
None, description="ISO timestamp when task execution ended"
|
||||
)
|
||||
|
||||
|
||||
class TaskStateMessage(BaseModel):
|
||||
history: Dict[str, TaskHistoryItem] = Field(
|
||||
..., description='Map of task IDs to their history items'
|
||||
..., description="Map of task IDs to their history items"
|
||||
)
|
||||
running_queue: List[QueueTaskItem] = Field(
|
||||
..., description='Currently executing tasks'
|
||||
..., description="Currently executing tasks"
|
||||
)
|
||||
pending_queue: List[QueueTaskItem] = Field(
|
||||
..., description='Tasks waiting to be executed'
|
||||
..., description="Tasks waiting to be executed"
|
||||
)
|
||||
installed_packs: Dict[str, ManagerPackInstalled] = Field(
|
||||
..., description='Map of currently installed node packages by name'
|
||||
..., description="Map of currently installed node packages by name"
|
||||
)
|
||||
|
||||
|
||||
class MessageTaskDone(BaseModel):
|
||||
ui_id: str = Field(..., description='Task identifier')
|
||||
result: str = Field(..., description='Task result message')
|
||||
kind: str = Field(..., description='Type of task')
|
||||
ui_id: str = Field(..., description="Task identifier")
|
||||
result: str = Field(..., description="Task result message")
|
||||
kind: str = Field(..., description="Type of task")
|
||||
status: Optional[TaskExecutionStatus] = None
|
||||
timestamp: datetime = Field(..., description='ISO timestamp when task completed')
|
||||
timestamp: datetime = Field(..., description="ISO timestamp when task completed")
|
||||
state: TaskStateMessage
|
||||
|
||||
|
||||
class MessageTaskStarted(BaseModel):
|
||||
ui_id: str = Field(..., description='Task identifier')
|
||||
kind: str = Field(..., description='Type of task')
|
||||
timestamp: datetime = Field(..., description='ISO timestamp when task started')
|
||||
ui_id: str = Field(..., description="Task identifier")
|
||||
kind: str = Field(..., description="Type of task")
|
||||
timestamp: datetime = Field(..., description="ISO timestamp when task started")
|
||||
state: TaskStateMessage
|
||||
|
||||
|
||||
class MessageTaskFailed(BaseModel):
|
||||
ui_id: str = Field(..., description='Task identifier')
|
||||
error: str = Field(..., description='Error message')
|
||||
kind: str = Field(..., description='Type of task')
|
||||
timestamp: datetime = Field(..., description='ISO timestamp when task failed')
|
||||
ui_id: str = Field(..., description="Task identifier")
|
||||
error: str = Field(..., description="Error message")
|
||||
kind: str = Field(..., description="Type of task")
|
||||
timestamp: datetime = Field(..., description="ISO timestamp when task failed")
|
||||
state: TaskStateMessage
|
||||
|
||||
|
||||
@@ -466,11 +547,15 @@ class MessageUpdate(
|
||||
RootModel[Union[MessageTaskDone, MessageTaskStarted, MessageTaskFailed]]
|
||||
):
|
||||
root: Union[MessageTaskDone, MessageTaskStarted, MessageTaskFailed] = Field(
|
||||
..., description='Union type for all possible WebSocket message updates'
|
||||
..., description="Union type for all possible WebSocket message updates"
|
||||
)
|
||||
|
||||
|
||||
class HistoryResponse(BaseModel):
|
||||
history: Optional[Dict[str, TaskHistoryItem]] = Field(
|
||||
None, description='Map of task IDs to their history items'
|
||||
None, description="Map of task IDs to their history items"
|
||||
)
|
||||
|
||||
|
||||
class ImportFailInfoBulkResponse(RootModel[Optional[Dict[str, ImportFailInfoItem]]]):
|
||||
root: Optional[Dict[str, ImportFailInfoItem]] = None
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,3 +8,4 @@
|
||||
7. Adjust the `__init__.py` files in the `data_models` directory to match/export the new data model
|
||||
8. Only then, make the changes to the rest of the codebase
|
||||
9. Run the CI tests to verify that the changes are working
|
||||
- The comfyui_manager is a python package that is used to manage the comfyui server. There are two sub-packages `glob` and `legacy`. These represent the current version (`glob`) and the previous version (`legacy`), not including common utilities and data models. When developing, we work in the `glob` package. You can ignore the `legacy` package entirely, unless you have a very good reason to research how things were done in the legacy or prior major versions of the package. But in those cases, you should just look for the sake of knowledge or reflection, not for changing code (unless explicitly asked to do so).
|
||||
@@ -1,6 +1,6 @@
|
||||
from comfy.cli_args import args
|
||||
|
||||
SECURITY_MESSAGE_MIDDLE_OR_BELOW = "ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_MIDDLE = "ERROR: To use this action, a security_level of `normal or below` is required. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_MIDDLE_P = "ERROR: To use this action, security_level must be `normal or below`, and network_mode must be set to `personal_cloud`. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_NORMAL_MINUS = "ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower."
|
||||
@@ -15,9 +15,6 @@ def is_loopback(address):
|
||||
return False
|
||||
|
||||
|
||||
is_local_mode = is_loopback(args.listen)
|
||||
|
||||
|
||||
model_dir_name_map = {
|
||||
"checkpoints": "checkpoints",
|
||||
"checkpoint": "checkpoints",
|
||||
@@ -37,3 +34,22 @@ model_dir_name_map = {
|
||||
"unet": "diffusion_models",
|
||||
"diffusion_model": "diffusion_models",
|
||||
}
|
||||
|
||||
# List of all model directory names used for checking installed models
|
||||
MODEL_DIR_NAMES = [
|
||||
"checkpoints",
|
||||
"loras",
|
||||
"vae",
|
||||
"text_encoders",
|
||||
"diffusion_models",
|
||||
"clip_vision",
|
||||
"embeddings",
|
||||
"diffusers",
|
||||
"vae_approx",
|
||||
"controlnet",
|
||||
"gligen",
|
||||
"upscale_models",
|
||||
"hypernetworks",
|
||||
"photomaker",
|
||||
"classifiers",
|
||||
]
|
||||
|
||||
@@ -12,9 +12,9 @@ import re
|
||||
import shutil
|
||||
import configparser
|
||||
import platform
|
||||
from datetime import datetime
|
||||
|
||||
import git
|
||||
from comfyui_manager.common.timestamp_utils import get_timestamp_for_path, get_backup_branch_name
|
||||
from git.remote import RemoteProgress
|
||||
from urllib.parse import urlparse
|
||||
from tqdm.auto import tqdm
|
||||
@@ -41,12 +41,15 @@ from ..common.enums import NetworkMode, SecurityLevel, DBMode
|
||||
from ..common import context
|
||||
|
||||
|
||||
version_code = [4, 0]
|
||||
version_code = [4, 0, 4]
|
||||
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
|
||||
|
||||
|
||||
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
|
||||
DEFAULT_CHANNEL_LEGACY = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
|
||||
|
||||
# SSH git URL pattern (e.g., git@github.com:user/repo.git)
|
||||
SSH_URL_PATTERN = re.compile(r"^(.+@|ssh://).+:.+$")
|
||||
|
||||
default_custom_nodes_path = None
|
||||
|
||||
@@ -153,14 +156,8 @@ def check_invalid_nodes():
|
||||
cached_config = None
|
||||
js_path = None
|
||||
|
||||
comfy_ui_required_revision = 1930
|
||||
comfy_ui_required_commit_datetime = datetime(2024, 1, 24, 0, 0, 0)
|
||||
|
||||
comfy_ui_revision = "Unknown"
|
||||
comfy_ui_commit_datetime = datetime(1900, 1, 1, 0, 0, 0)
|
||||
|
||||
channel_dict = None
|
||||
valid_channels = {'default', 'local'}
|
||||
valid_channels = {'default', 'local', DEFAULT_CHANNEL, DEFAULT_CHANNEL_LEGACY}
|
||||
channel_list = None
|
||||
|
||||
|
||||
@@ -304,29 +301,108 @@ class ManagedResult:
|
||||
return self
|
||||
|
||||
|
||||
class NormalizedKeyDict:
|
||||
def __init__(self):
|
||||
self._store = {}
|
||||
self._key_map = {}
|
||||
|
||||
def _normalize_key(self, key):
|
||||
if isinstance(key, str):
|
||||
return key.strip().lower()
|
||||
return key
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
norm_key = self._normalize_key(key)
|
||||
self._key_map[norm_key] = key
|
||||
self._store[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
norm_key = self._normalize_key(key)
|
||||
original_key = self._key_map[norm_key]
|
||||
return self._store[original_key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
norm_key = self._normalize_key(key)
|
||||
original_key = self._key_map.pop(norm_key)
|
||||
del self._store[original_key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return self._normalize_key(key) in self._key_map
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key] if key in self else default
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
if key in self:
|
||||
return self[key]
|
||||
self[key] = default
|
||||
return default
|
||||
|
||||
def pop(self, key, default=None):
|
||||
if key in self:
|
||||
val = self[key]
|
||||
del self[key]
|
||||
return val
|
||||
if default is not None:
|
||||
return default
|
||||
raise KeyError(key)
|
||||
|
||||
def keys(self):
|
||||
return self._store.keys()
|
||||
|
||||
def values(self):
|
||||
return self._store.values()
|
||||
|
||||
def items(self):
|
||||
return self._store.items()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._store)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._store)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self._store)
|
||||
|
||||
def to_dict(self):
|
||||
return dict(self._store)
|
||||
|
||||
|
||||
class UnifiedManager:
|
||||
def __init__(self):
|
||||
self.installed_node_packages: dict[str, InstalledNodePackage] = {}
|
||||
|
||||
self.cnr_inactive_nodes = {} # node_id -> node_version -> fullpath
|
||||
self.nightly_inactive_nodes = {} # node_id -> fullpath
|
||||
self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath
|
||||
self.active_nodes = {} # node_id -> node_version * fullpath
|
||||
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
|
||||
self.cnr_map = {} # node_id -> cnr info
|
||||
self.repo_cnr_map = {} # repo_url -> cnr info
|
||||
self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json
|
||||
self.cnr_inactive_nodes = NormalizedKeyDict() # node_id -> node_version -> fullpath
|
||||
self.nightly_inactive_nodes = NormalizedKeyDict() # node_id -> fullpath
|
||||
self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath
|
||||
self.active_nodes = NormalizedKeyDict() # node_id -> node_version * fullpath
|
||||
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
|
||||
self.cnr_map = NormalizedKeyDict() # node_id -> cnr info
|
||||
self.repo_cnr_map = {} # repo_url -> cnr info
|
||||
self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json
|
||||
self.processed_install = set()
|
||||
|
||||
def get_module_name(self, x):
|
||||
# 1. Direct cnr_id lookup
|
||||
info = self.active_nodes.get(x)
|
||||
if info is None:
|
||||
for url, fullpath in self.unknown_active_nodes.values():
|
||||
if url == x:
|
||||
return os.path.basename(fullpath)
|
||||
else:
|
||||
if info is not None:
|
||||
return os.path.basename(info[1])
|
||||
|
||||
# 2. URL/aux_id → cnr_id conversion via repo_cnr_map
|
||||
cnr_info = self.get_cnr_by_repo(x)
|
||||
if cnr_info is not None:
|
||||
cnr_id = cnr_info['id']
|
||||
info = self.active_nodes.get(cnr_id)
|
||||
if info is not None:
|
||||
return os.path.basename(info[1])
|
||||
|
||||
# 3. Fallback: search unknown_active_nodes by URL
|
||||
normalized_x = git_utils.normalize_url(x)
|
||||
for url, fullpath in self.unknown_active_nodes.values():
|
||||
if url is not None and git_utils.normalize_url(url) == normalized_x:
|
||||
return os.path.basename(fullpath)
|
||||
|
||||
return None
|
||||
|
||||
def get_cnr_by_repo(self, url):
|
||||
@@ -637,11 +713,11 @@ class UnifiedManager:
|
||||
import folder_paths
|
||||
|
||||
self.custom_node_map_cache = {}
|
||||
self.cnr_inactive_nodes = {} # node_id -> node_version -> fullpath
|
||||
self.nightly_inactive_nodes = {} # node_id -> fullpath
|
||||
self.cnr_inactive_nodes = NormalizedKeyDict() # node_id -> node_version -> fullpath
|
||||
self.nightly_inactive_nodes = NormalizedKeyDict() # node_id -> fullpath
|
||||
self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath
|
||||
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
|
||||
self.active_nodes = {} # node_id -> node_version * fullpath
|
||||
self.active_nodes = NormalizedKeyDict() # node_id -> node_version * fullpath
|
||||
|
||||
if get_config()['network_mode'] != 'public' or manager_util.is_manager_pip_package():
|
||||
dont_wait = True
|
||||
@@ -721,7 +797,7 @@ class UnifiedManager:
|
||||
channel = normalize_channel(channel)
|
||||
nodes = await self.load_nightly(channel, mode)
|
||||
|
||||
res = {}
|
||||
res = NormalizedKeyDict()
|
||||
added_cnr = set()
|
||||
for v in nodes.values():
|
||||
v = v[0]
|
||||
@@ -940,7 +1016,6 @@ class UnifiedManager:
|
||||
"""
|
||||
|
||||
result = ManagedResult('enable')
|
||||
|
||||
if 'comfyui-manager' in node_id.lower():
|
||||
return result.fail(f"ignored: enabling '{node_id}'")
|
||||
|
||||
@@ -1411,7 +1486,7 @@ def identify_node_pack_from_path(fullpath):
|
||||
# cnr
|
||||
cnr = cnr_utils.read_cnr_info(fullpath)
|
||||
if cnr is not None:
|
||||
return module_name, cnr['version'], cnr['id'], None
|
||||
return module_name, cnr['version'], cnr['original_name'], None
|
||||
|
||||
return None
|
||||
else:
|
||||
@@ -1461,7 +1536,10 @@ def get_installed_node_packs():
|
||||
if info is None:
|
||||
continue
|
||||
|
||||
res[info[0]] = { 'ver': info[1], 'cnr_id': info[2], 'aux_id': info[3], 'enabled': False }
|
||||
# NOTE: don't add disabled nodepack if there is enabled nodepack
|
||||
original_name = info[0].split('@')[0]
|
||||
if original_name not in res:
|
||||
res[info[0]] = { 'ver': info[1], 'cnr_id': info[2], 'aux_id': info[3], 'enabled': False }
|
||||
|
||||
return res
|
||||
|
||||
@@ -1507,9 +1585,6 @@ class ManagerFuncs:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_current_preview_method(self):
|
||||
return "none"
|
||||
|
||||
def run_script(self, cmd, cwd='.'):
|
||||
if len(cmd) > 0 and cmd[0].startswith("#"):
|
||||
print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`")
|
||||
@@ -1527,14 +1602,12 @@ def write_config():
|
||||
config = configparser.ConfigParser(strict=False)
|
||||
|
||||
config['default'] = {
|
||||
'preview_method': manager_funcs.get_current_preview_method(),
|
||||
'git_exe': get_config()['git_exe'],
|
||||
'use_uv': get_config()['use_uv'],
|
||||
'channel_url': get_config()['channel_url'],
|
||||
'share_option': get_config()['share_option'],
|
||||
'bypass_ssl': get_config()['bypass_ssl'],
|
||||
"file_logging": get_config()['file_logging'],
|
||||
'component_policy': get_config()['component_policy'],
|
||||
'update_policy': get_config()['update_policy'],
|
||||
'windows_selector_event_loop_policy': get_config()['windows_selector_event_loop_policy'],
|
||||
'model_download_by_agent': get_config()['model_download_by_agent'],
|
||||
@@ -1543,6 +1616,7 @@ def write_config():
|
||||
'always_lazy_install': get_config()['always_lazy_install'],
|
||||
'network_mode': get_config()['network_mode'],
|
||||
'db_mode': get_config()['db_mode'],
|
||||
'verbose': get_config()['verbose'],
|
||||
}
|
||||
|
||||
directory = os.path.dirname(context.manager_config_path)
|
||||
@@ -1558,22 +1632,22 @@ def read_config():
|
||||
config = configparser.ConfigParser(strict=False)
|
||||
config.read(context.manager_config_path)
|
||||
default_conf = config['default']
|
||||
manager_util.use_uv = default_conf['use_uv'].lower() == 'true' if 'use_uv' in default_conf else False
|
||||
|
||||
def get_bool(key, default_value):
|
||||
return default_conf[key].lower() == 'true' if key in default_conf else False
|
||||
|
||||
manager_util.use_uv = default_conf['use_uv'].lower() == 'true' if 'use_uv' in default_conf else False
|
||||
manager_util.bypass_ssl = get_bool('bypass_ssl', False)
|
||||
|
||||
return {
|
||||
'http_channel_enabled': get_bool('http_channel_enabled', False),
|
||||
'preview_method': default_conf.get('preview_method', manager_funcs.get_current_preview_method()).lower(),
|
||||
'git_exe': default_conf.get('git_exe', ''),
|
||||
'use_uv': get_bool('use_uv', False),
|
||||
'use_uv': get_bool('use_uv', True),
|
||||
'channel_url': default_conf.get('channel_url', DEFAULT_CHANNEL),
|
||||
'default_cache_as_channel_url': get_bool('default_cache_as_channel_url', False),
|
||||
'share_option': default_conf.get('share_option', 'all').lower(),
|
||||
'bypass_ssl': get_bool('bypass_ssl', False),
|
||||
'file_logging': get_bool('file_logging', True),
|
||||
'component_policy': default_conf.get('component_policy', 'workflow').lower(),
|
||||
'update_policy': default_conf.get('update_policy', 'stable-comfyui').lower(),
|
||||
'windows_selector_event_loop_policy': get_bool('windows_selector_event_loop_policy', False),
|
||||
'model_download_by_agent': get_bool('model_download_by_agent', False),
|
||||
@@ -1582,21 +1656,24 @@ def read_config():
|
||||
'network_mode': default_conf.get('network_mode', NetworkMode.PUBLIC.value).lower(),
|
||||
'security_level': default_conf.get('security_level', SecurityLevel.NORMAL.value).lower(),
|
||||
'db_mode': default_conf.get('db_mode', DBMode.CACHE.value).lower(),
|
||||
'verbose': get_bool('verbose', False),
|
||||
}
|
||||
|
||||
except Exception:
|
||||
manager_util.use_uv = False
|
||||
import importlib.util
|
||||
# temporary disable `uv` on Windows by default (https://github.com/Comfy-Org/ComfyUI-Manager/issues/1969)
|
||||
manager_util.use_uv = importlib.util.find_spec("uv") is not None and platform.system() != "Windows"
|
||||
manager_util.bypass_ssl = False
|
||||
|
||||
return {
|
||||
'http_channel_enabled': False,
|
||||
'preview_method': manager_funcs.get_current_preview_method(),
|
||||
'git_exe': '',
|
||||
'use_uv': False,
|
||||
'use_uv': manager_util.use_uv,
|
||||
'channel_url': DEFAULT_CHANNEL,
|
||||
'default_cache_as_channel_url': False,
|
||||
'share_option': 'all',
|
||||
'bypass_ssl': False,
|
||||
'bypass_ssl': manager_util.bypass_ssl,
|
||||
'file_logging': True,
|
||||
'component_policy': 'workflow',
|
||||
'update_policy': 'stable-comfyui',
|
||||
'windows_selector_event_loop_policy': False,
|
||||
'model_download_by_agent': False,
|
||||
@@ -1605,6 +1682,7 @@ def read_config():
|
||||
'network_mode': NetworkMode.PUBLIC.value,
|
||||
'security_level': SecurityLevel.NORMAL.value,
|
||||
'db_mode': DBMode.CACHE.value,
|
||||
'verbose': False,
|
||||
}
|
||||
|
||||
|
||||
@@ -1712,16 +1790,6 @@ def try_install_script(url, repo_path, install_cmd, instant_execution=False):
|
||||
print(f"\n## ComfyUI-Manager: EXECUTE => {install_cmd}")
|
||||
code = manager_funcs.run_script(install_cmd, cwd=repo_path)
|
||||
|
||||
if platform.system() != "Windows":
|
||||
try:
|
||||
if not os.environ.get('__COMFYUI_DESKTOP_VERSION__') and comfy_ui_commit_datetime.date() < comfy_ui_required_commit_datetime.date():
|
||||
print("\n\n###################################################################")
|
||||
print(f"[WARN] ComfyUI-Manager: Your ComfyUI version ({comfy_ui_revision})[{comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version.")
|
||||
print("[WARN] The extension installation feature may not work properly in the current installed ComfyUI version on Windows environment.")
|
||||
print("###################################################################\n\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if code != 0:
|
||||
if url is None:
|
||||
url = os.path.dirname(repo_path)
|
||||
@@ -1840,6 +1908,27 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
|
||||
return True
|
||||
|
||||
|
||||
def install_manager_requirements(repo_path):
|
||||
"""
|
||||
Install packages from manager_requirements.txt if it exists.
|
||||
This is specifically for ComfyUI's manager_requirements.txt.
|
||||
"""
|
||||
manager_requirements_path = os.path.join(repo_path, "manager_requirements.txt")
|
||||
if not os.path.exists(manager_requirements_path):
|
||||
return
|
||||
|
||||
logging.info("[ComfyUI-Manager] Installing manager_requirements.txt")
|
||||
with open(manager_requirements_path, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
if '#' in line:
|
||||
line = line.split('#')[0].strip()
|
||||
if line:
|
||||
install_cmd = manager_util.make_pip_cmd(["install", line])
|
||||
subprocess.run(install_cmd)
|
||||
|
||||
|
||||
def git_repo_update_check_with(path, do_fetch=False, do_update=False, no_deps=False):
|
||||
"""
|
||||
|
||||
@@ -1918,7 +2007,15 @@ def git_repo_update_check_with(path, do_fetch=False, do_update=False, no_deps=Fa
|
||||
return False, True
|
||||
|
||||
try:
|
||||
remote.pull()
|
||||
try:
|
||||
repo.git.pull('--ff-only')
|
||||
except git.GitCommandError:
|
||||
backup_name = get_backup_branch_name(repo)
|
||||
repo.create_head(backup_name)
|
||||
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
|
||||
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
|
||||
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
|
||||
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
new_commit_hash = repo.head.commit.hexsha
|
||||
|
||||
@@ -1975,16 +2072,15 @@ class GitProgress(RemoteProgress):
|
||||
|
||||
|
||||
def is_valid_url(url):
|
||||
try:
|
||||
# Check for HTTP/HTTPS URL format
|
||||
result = urlparse(url)
|
||||
if all([result.scheme, result.netloc]):
|
||||
return True
|
||||
finally:
|
||||
# Check for SSH git URL format
|
||||
pattern = re.compile(r"^(.+@|ssh://).+:.+$")
|
||||
if pattern.match(url):
|
||||
return True
|
||||
# Check for HTTP/HTTPS URL format
|
||||
result = urlparse(url)
|
||||
if result.scheme and result.netloc:
|
||||
return True
|
||||
|
||||
# Check for SSH git URL format
|
||||
if SSH_URL_PATTERN.match(url):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -2087,9 +2183,17 @@ def git_pull(path):
|
||||
|
||||
current_branch = repo.active_branch
|
||||
remote_name = current_branch.tracking_branch().remote_name
|
||||
remote = repo.remote(name=remote_name)
|
||||
branch_name = current_branch.name
|
||||
|
||||
try:
|
||||
repo.git.pull('--ff-only')
|
||||
except git.GitCommandError:
|
||||
backup_name = get_backup_branch_name(repo)
|
||||
repo.create_head(backup_name)
|
||||
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
|
||||
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
|
||||
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
|
||||
|
||||
remote.pull()
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
|
||||
repo.close()
|
||||
@@ -2357,22 +2461,23 @@ def update_to_stable_comfyui(repo_path):
|
||||
logging.error('\t'+branch.name)
|
||||
return "fail", None
|
||||
|
||||
versions, current_tag, _ = get_comfyui_versions(repo)
|
||||
|
||||
if len(versions) == 0 or (len(versions) == 1 and versions[0] == 'nightly'):
|
||||
versions, current_tag, latest_tag = get_comfyui_versions(repo)
|
||||
|
||||
if latest_tag is None:
|
||||
logging.info("[ComfyUI-Manager] Unable to update to the stable ComfyUI version.")
|
||||
return "fail", None
|
||||
|
||||
if versions[0] == 'nightly':
|
||||
latest_tag = versions[1]
|
||||
else:
|
||||
latest_tag = versions[0]
|
||||
|
||||
if current_tag == latest_tag:
|
||||
tag_ref = next((t for t in repo.tags if t.name == latest_tag), None)
|
||||
if tag_ref is None:
|
||||
logging.info(f"[ComfyUI-Manager] Unable to locate tag '{latest_tag}' in repository.")
|
||||
return "fail", None
|
||||
|
||||
if repo.head.commit == tag_ref.commit:
|
||||
return "skip", None
|
||||
else:
|
||||
logging.info(f"[ComfyUI-Manager] Updating ComfyUI: {current_tag} -> {latest_tag}")
|
||||
repo.git.checkout(latest_tag)
|
||||
repo.git.checkout(tag_ref.name)
|
||||
execute_install_script("ComfyUI", repo_path, instant_execution=False, no_deps=False)
|
||||
return 'updated', latest_tag
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
@@ -2598,9 +2703,7 @@ async def get_current_snapshot(custom_nodes_only = False):
|
||||
|
||||
async def save_snapshot_with_postfix(postfix, path=None, custom_nodes_only = False):
|
||||
if path is None:
|
||||
now = datetime.now()
|
||||
|
||||
date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
|
||||
date_time_format = get_timestamp_for_path()
|
||||
file_name = f"{date_time_format}_{postfix}"
|
||||
|
||||
path = os.path.join(context.manager_snapshot_path, f"{file_name}.json")
|
||||
@@ -2791,7 +2894,7 @@ async def get_unified_total_nodes(channel, mode, regsitry_cache_mode='cache'):
|
||||
|
||||
if cnr_id is not None:
|
||||
# cnr or nightly version
|
||||
cnr_ids.remove(cnr_id)
|
||||
cnr_ids.discard(cnr_id)
|
||||
updatable = False
|
||||
cnr = unified_manager.cnr_map[cnr_id]
|
||||
|
||||
@@ -2955,6 +3058,11 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
|
||||
info = yaml.load(snapshot_file, Loader=yaml.SafeLoader)
|
||||
info = info['custom_nodes']
|
||||
|
||||
if 'pips' in info and info['pips']:
|
||||
pips = info['pips']
|
||||
else:
|
||||
pips = {}
|
||||
|
||||
# for cnr restore
|
||||
cnr_info = info.get('cnr_custom_nodes')
|
||||
if cnr_info is not None:
|
||||
@@ -3161,6 +3269,8 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
|
||||
unified_manager.repo_install(repo_url, to_path, instant_execution=True, no_deps=False, return_postinstall=False)
|
||||
cloned_repos.append(repo_name)
|
||||
|
||||
manager_util.restore_pip_snapshot(pips, git_helper_extras)
|
||||
|
||||
# print summary
|
||||
for x in cloned_repos:
|
||||
print(f"[ INSTALLED ] {x}")
|
||||
@@ -3184,36 +3294,85 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
|
||||
|
||||
|
||||
def get_comfyui_versions(repo=None):
|
||||
if repo is None:
|
||||
repo = git.Repo(context.comfy_path)
|
||||
repo = repo or git.Repo(context.comfy_path)
|
||||
|
||||
remote_name = None
|
||||
try:
|
||||
remote = get_remote_name(repo)
|
||||
repo.remotes[remote].fetch()
|
||||
remote_name = get_remote_name(repo)
|
||||
repo.remotes[remote_name].fetch()
|
||||
except Exception:
|
||||
logging.error("[ComfyUI-Manager] Failed to fetch ComfyUI")
|
||||
|
||||
versions = [x.name for x in repo.tags if x.name.startswith('v')]
|
||||
def parse_semver(tag_name):
|
||||
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', tag_name)
|
||||
return tuple(int(x) for x in match.groups()) if match else None
|
||||
|
||||
# nearest tag
|
||||
versions = sorted(versions, key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
|
||||
versions = versions[:4]
|
||||
def normalize_describe(tag_name):
|
||||
if not tag_name:
|
||||
return None
|
||||
base = tag_name.split('-', 1)[0]
|
||||
return base if parse_semver(base) else None
|
||||
|
||||
current_tag = repo.git.describe('--tags')
|
||||
# Collect semver tags and sort descending (highest first)
|
||||
semver_tags = []
|
||||
for tag in repo.tags:
|
||||
semver = parse_semver(tag.name)
|
||||
if semver:
|
||||
semver_tags.append((semver, tag.name))
|
||||
semver_tags.sort(key=lambda x: x[0], reverse=True)
|
||||
semver_tags = [name for _, name in semver_tags]
|
||||
|
||||
if current_tag not in versions:
|
||||
versions = sorted(versions + [current_tag], key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
|
||||
versions = versions[:4]
|
||||
latest_tag = semver_tags[0] if semver_tags else None
|
||||
|
||||
main_branch = repo.heads.master
|
||||
latest_commit = main_branch.commit
|
||||
latest_tag = repo.git.describe('--tags', latest_commit.hexsha)
|
||||
try:
|
||||
described = repo.git.describe('--tags')
|
||||
except Exception:
|
||||
described = ''
|
||||
|
||||
if latest_tag != versions[0]:
|
||||
versions.insert(0, 'nightly')
|
||||
else:
|
||||
versions[0] = 'nightly'
|
||||
try:
|
||||
exact_tag = repo.git.describe('--tags', '--exact-match')
|
||||
except Exception:
|
||||
exact_tag = ''
|
||||
|
||||
head_is_default = False
|
||||
if remote_name:
|
||||
try:
|
||||
default_head_ref = repo.refs[f'{remote_name}/HEAD']
|
||||
default_commit = default_head_ref.reference.commit
|
||||
head_is_default = repo.head.commit == default_commit
|
||||
except Exception:
|
||||
# Fallback: compare directly with master branch
|
||||
try:
|
||||
if 'master' in [h.name for h in repo.heads]:
|
||||
head_is_default = repo.head.commit == repo.heads.master.commit
|
||||
except Exception:
|
||||
head_is_default = False
|
||||
|
||||
nearest_semver = normalize_describe(described)
|
||||
exact_semver = exact_tag if parse_semver(exact_tag) else None
|
||||
|
||||
if head_is_default and not exact_tag:
|
||||
current_tag = 'nightly'
|
||||
else:
|
||||
current_tag = exact_tag or described or 'nightly'
|
||||
|
||||
# Prepare semver list for display: top 4 plus the current/nearest semver if missing
|
||||
display_semver_tags = semver_tags[:4]
|
||||
if exact_semver and exact_semver not in display_semver_tags:
|
||||
display_semver_tags.append(exact_semver)
|
||||
elif nearest_semver and nearest_semver not in display_semver_tags:
|
||||
display_semver_tags.append(nearest_semver)
|
||||
|
||||
versions = ['nightly']
|
||||
|
||||
if current_tag and not exact_semver and current_tag not in versions and current_tag not in display_semver_tags:
|
||||
versions.append(current_tag)
|
||||
|
||||
for tag in display_semver_tags:
|
||||
if tag not in versions:
|
||||
versions.append(tag)
|
||||
|
||||
versions = versions[:6]
|
||||
|
||||
return versions, current_tag, latest_tag
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,15 @@ import hashlib
|
||||
import folder_paths
|
||||
from server import PromptServer
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
try:
|
||||
from nio import AsyncClient, LoginResponse, UploadResponse
|
||||
matrix_nio_is_available = True
|
||||
except Exception:
|
||||
logging.warning(f"[ComfyUI-Manager] The matrix sharing feature has been disabled because the `matrix-nio` dependency is not installed.\n\tTo use this feature, please run the following command:\n\t{sys.executable} -m pip install matrix-nio\n")
|
||||
matrix_nio_is_available = False
|
||||
|
||||
|
||||
def extract_model_file_names(json_data):
|
||||
@@ -193,6 +202,14 @@ async def get_esheep_workflow_and_images(request):
|
||||
return web.Response(status=200, text=json.dumps(data))
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/v2/manager/get_matrix_dep_status")
|
||||
async def get_matrix_dep_status(request):
|
||||
if matrix_nio_is_available:
|
||||
return web.Response(status=200, text='available')
|
||||
else:
|
||||
return web.Response(status=200, text='unavailable')
|
||||
|
||||
|
||||
def set_matrix_auth(json_data):
|
||||
homeserver = json_data['homeserver']
|
||||
username = json_data['username']
|
||||
@@ -332,15 +349,12 @@ async def share_art(request):
|
||||
workflowId = upload_workflow_json["workflowId"]
|
||||
|
||||
# check if the user has provided Matrix credentials
|
||||
if "matrix" in share_destinations:
|
||||
if matrix_nio_is_available and "matrix" in share_destinations:
|
||||
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
|
||||
filename = os.path.basename(asset_filepath)
|
||||
content_type = assetFileType
|
||||
|
||||
try:
|
||||
from matrix_client.api import MatrixHttpApi
|
||||
from matrix_client.client import MatrixClient
|
||||
|
||||
homeserver = 'matrix.org'
|
||||
if matrix_auth:
|
||||
homeserver = matrix_auth.get('homeserver', 'matrix.org')
|
||||
@@ -348,20 +362,35 @@ async def share_art(request):
|
||||
if not homeserver.startswith("https://"):
|
||||
homeserver = "https://" + homeserver
|
||||
|
||||
client = MatrixClient(homeserver)
|
||||
try:
|
||||
token = client.login(username=matrix_auth['username'], password=matrix_auth['password'])
|
||||
if not token:
|
||||
return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
|
||||
except Exception:
|
||||
client = AsyncClient(homeserver, matrix_auth['username'])
|
||||
|
||||
# Login
|
||||
login_resp = await client.login(matrix_auth['password'])
|
||||
if not isinstance(login_resp, LoginResponse) or not login_resp.access_token:
|
||||
await client.close()
|
||||
return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
|
||||
|
||||
matrix = MatrixHttpApi(homeserver, token=token)
|
||||
# Upload asset
|
||||
with open(asset_filepath, 'rb') as f:
|
||||
mxc_url = matrix.media_upload(f.read(), content_type, filename=filename)['content_uri']
|
||||
upload_resp, _maybe_keys = await client.upload(f, content_type=content_type, filename=filename)
|
||||
asset_data = f.seek(0) or f.read() # get size for info below
|
||||
if not isinstance(upload_resp, UploadResponse) or not upload_resp.content_uri:
|
||||
await client.close()
|
||||
return web.json_response({"error": "Failed to upload asset to Matrix."}, content_type='application/json', status=500)
|
||||
mxc_url = upload_resp.content_uri
|
||||
|
||||
workflow_json_mxc_url = matrix.media_upload(prompt['workflow'], 'application/json', filename='workflow.json')['content_uri']
|
||||
# Upload workflow JSON
|
||||
import io
|
||||
workflow_json_bytes = json.dumps(prompt['workflow']).encode('utf-8')
|
||||
workflow_io = io.BytesIO(workflow_json_bytes)
|
||||
upload_workflow_resp, _maybe_keys = await client.upload(workflow_io, content_type='application/json', filename='workflow.json')
|
||||
workflow_io.seek(0)
|
||||
if not isinstance(upload_workflow_resp, UploadResponse) or not upload_workflow_resp.content_uri:
|
||||
await client.close()
|
||||
return web.json_response({"error": "Failed to upload workflow to Matrix."}, content_type='application/json', status=500)
|
||||
workflow_json_mxc_url = upload_workflow_resp.content_uri
|
||||
|
||||
# Send text message
|
||||
text_content = ""
|
||||
if title:
|
||||
text_content += f"{title}\n"
|
||||
@@ -369,11 +398,47 @@ async def share_art(request):
|
||||
text_content += f"{description}\n"
|
||||
if credits:
|
||||
text_content += f"\ncredits: {credits}\n"
|
||||
matrix.send_message(comfyui_share_room_id, text_content)
|
||||
matrix.send_content(comfyui_share_room_id, mxc_url, filename, 'm.image')
|
||||
matrix.send_content(comfyui_share_room_id, workflow_json_mxc_url, 'workflow.json', 'm.file')
|
||||
except Exception:
|
||||
logging.exception("An error occurred")
|
||||
await client.room_send(
|
||||
room_id=comfyui_share_room_id,
|
||||
message_type="m.room.message",
|
||||
content={"msgtype": "m.text", "body": text_content}
|
||||
)
|
||||
|
||||
# Send image
|
||||
await client.room_send(
|
||||
room_id=comfyui_share_room_id,
|
||||
message_type="m.room.message",
|
||||
content={
|
||||
"msgtype": "m.image",
|
||||
"body": filename,
|
||||
"url": mxc_url,
|
||||
"info": {
|
||||
"mimetype": content_type,
|
||||
"size": len(asset_data)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Send workflow JSON file
|
||||
await client.room_send(
|
||||
room_id=comfyui_share_room_id,
|
||||
message_type="m.room.message",
|
||||
content={
|
||||
"msgtype": "m.file",
|
||||
"body": "workflow.json",
|
||||
"url": workflow_json_mxc_url,
|
||||
"info": {
|
||||
"mimetype": "application/json",
|
||||
"size": len(workflow_json_bytes)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await client.close()
|
||||
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"error": "An error occurred when sharing your art to Matrix."}, content_type='application/json', status=500)
|
||||
|
||||
return web.json_response({
|
||||
|
||||
@@ -5,8 +5,6 @@ import traceback
|
||||
|
||||
from comfyui_manager.common import context
|
||||
import folder_paths
|
||||
from comfy.cli_args import args
|
||||
import latent_preview
|
||||
|
||||
from comfyui_manager.glob import manager_core as core
|
||||
from comfyui_manager.common import cm_global
|
||||
@@ -93,19 +91,6 @@ def print_comfyui_version():
|
||||
)
|
||||
|
||||
|
||||
def set_preview_method(method):
|
||||
if method == "auto":
|
||||
args.preview_method = latent_preview.LatentPreviewMethod.Auto
|
||||
elif method == "latent2rgb":
|
||||
args.preview_method = latent_preview.LatentPreviewMethod.Latent2RGB
|
||||
elif method == "taesd":
|
||||
args.preview_method = latent_preview.LatentPreviewMethod.TAESD
|
||||
else:
|
||||
args.preview_method = latent_preview.LatentPreviewMethod.NoPreviews
|
||||
|
||||
core.get_config()["preview_method"] = method
|
||||
|
||||
|
||||
def set_update_policy(mode):
|
||||
core.get_config()["update_policy"] = mode
|
||||
|
||||
@@ -135,7 +120,6 @@ def initialize_environment():
|
||||
# manager_util.comfyui_manager_path, "extension-node-map.json"
|
||||
# )
|
||||
|
||||
set_preview_method(core.get_config()["preview_method"])
|
||||
print_comfyui_version()
|
||||
setup_environment()
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import logging
|
||||
import concurrent.futures
|
||||
import folder_paths
|
||||
|
||||
from comfyui_manager.glob import manager_core as core
|
||||
from comfyui_manager.glob.constants import model_dir_name_map
|
||||
from comfyui_manager.glob.constants import model_dir_name_map, MODEL_DIR_NAMES
|
||||
|
||||
|
||||
def get_model_dir(data, show_log=False):
|
||||
@@ -72,3 +73,89 @@ def get_model_path(data, show_log=False):
|
||||
return os.path.join(base_model, os.path.basename(data["url"]))
|
||||
else:
|
||||
return os.path.join(base_model, data["filename"])
|
||||
|
||||
|
||||
def check_model_installed(json_obj):
|
||||
def is_exists(model_dir_name, filename, url):
|
||||
if filename == "<huggingface>":
|
||||
filename = os.path.basename(url)
|
||||
|
||||
dirs = folder_paths.get_folder_paths(model_dir_name)
|
||||
|
||||
for x in dirs:
|
||||
if os.path.exists(os.path.join(x, filename)):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
total_models_files = set()
|
||||
for x in MODEL_DIR_NAMES:
|
||||
for y in folder_paths.get_filename_list(x):
|
||||
total_models_files.add(y)
|
||||
|
||||
def process_model_phase(item):
|
||||
if (
|
||||
"diffusion" not in item["filename"]
|
||||
and "pytorch" not in item["filename"]
|
||||
and "model" not in item["filename"]
|
||||
):
|
||||
# non-general name case
|
||||
if item["filename"] in total_models_files:
|
||||
item["installed"] = "True"
|
||||
return
|
||||
|
||||
if item["save_path"] == "default":
|
||||
model_dir_name = model_dir_name_map.get(item["type"].lower())
|
||||
if model_dir_name is not None:
|
||||
item["installed"] = str(
|
||||
is_exists(model_dir_name, item["filename"], item["url"])
|
||||
)
|
||||
else:
|
||||
item["installed"] = "False"
|
||||
else:
|
||||
model_dir_name = item["save_path"].split("/")[0]
|
||||
if model_dir_name in folder_paths.folder_names_and_paths:
|
||||
if is_exists(model_dir_name, item["filename"], item["url"]):
|
||||
item["installed"] = "True"
|
||||
|
||||
if "installed" not in item:
|
||||
if item["filename"] == "<huggingface>":
|
||||
filename = os.path.basename(item["url"])
|
||||
else:
|
||||
filename = item["filename"]
|
||||
|
||||
fullpath = os.path.join(
|
||||
folder_paths.models_dir, item["save_path"], filename
|
||||
)
|
||||
|
||||
item["installed"] = "True" if os.path.exists(fullpath) else "False"
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(8) as executor:
|
||||
for item in json_obj["models"]:
|
||||
executor.submit(process_model_phase, item)
|
||||
|
||||
|
||||
async def check_whitelist_for_model(item):
|
||||
from comfyui_manager.data_models import ManagerDatabaseSource
|
||||
|
||||
json_obj = await core.get_data_by_mode(ManagerDatabaseSource.cache.value, "model-list.json")
|
||||
|
||||
for x in json_obj.get("models", []):
|
||||
if (
|
||||
x["save_path"] == item["save_path"]
|
||||
and x["base"] == item["base"]
|
||||
and x["filename"] == item["filename"]
|
||||
):
|
||||
return True
|
||||
|
||||
json_obj = await core.get_data_by_mode(ManagerDatabaseSource.local.value, "model-list.json")
|
||||
|
||||
for x in json_obj.get("models", []):
|
||||
if (
|
||||
x["save_path"] == item["save_path"]
|
||||
and x["base"] == item["base"]
|
||||
and x["filename"] == item["filename"]
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from comfyui_manager.glob import manager_core as core
|
||||
from comfy.cli_args import args
|
||||
from comfyui_manager.data_models import SecurityLevel, RiskLevel, ManagerDatabaseSource
|
||||
|
||||
|
||||
def is_loopback(address):
|
||||
@@ -12,24 +13,37 @@ def is_loopback(address):
|
||||
|
||||
def is_allowed_security_level(level):
|
||||
is_local_mode = is_loopback(args.listen)
|
||||
|
||||
if level == "block":
|
||||
is_personal_cloud = core.get_config()['network_mode'].lower() == 'personal_cloud'
|
||||
|
||||
if level == RiskLevel.block.value:
|
||||
return False
|
||||
elif level == "high":
|
||||
elif level == RiskLevel.high_.value:
|
||||
if is_local_mode:
|
||||
return core.get_config()["security_level"] in ["weak", "normal-"]
|
||||
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal_.value]
|
||||
elif is_personal_cloud:
|
||||
return core.get_config()['security_level'] == SecurityLevel.weak.value
|
||||
else:
|
||||
return core.get_config()["security_level"] == "weak"
|
||||
elif level == "middle":
|
||||
return core.get_config()["security_level"] in ["weak", "normal", "normal-"]
|
||||
return False
|
||||
elif level == RiskLevel.high.value:
|
||||
if is_local_mode:
|
||||
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal_.value]
|
||||
else:
|
||||
return core.get_config()['security_level'] == SecurityLevel.weak.value
|
||||
elif level == RiskLevel.middle_.value:
|
||||
if is_local_mode or is_personal_cloud:
|
||||
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal.value, SecurityLevel.normal_.value]
|
||||
else:
|
||||
return False
|
||||
elif level == RiskLevel.middle.value:
|
||||
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal.value, SecurityLevel.normal_.value]
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
async def get_risky_level(files, pip_packages):
|
||||
json_data1 = await core.get_data_by_mode("local", "custom-node-list.json")
|
||||
json_data1 = await core.get_data_by_mode(ManagerDatabaseSource.local.value, "custom-node-list.json")
|
||||
json_data2 = await core.get_data_by_mode(
|
||||
"cache",
|
||||
ManagerDatabaseSource.cache.value,
|
||||
"custom-node-list.json",
|
||||
channel_url="https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main",
|
||||
)
|
||||
@@ -40,7 +54,7 @@ async def get_risky_level(files, pip_packages):
|
||||
|
||||
for x in files:
|
||||
if x not in all_urls:
|
||||
return "high"
|
||||
return RiskLevel.high_.value
|
||||
|
||||
all_pip_packages = set()
|
||||
for x in json_data1["custom_nodes"] + json_data2["custom_nodes"]:
|
||||
@@ -48,6 +62,6 @@ async def get_risky_level(files, pip_packages):
|
||||
|
||||
for p in pip_packages:
|
||||
if p not in all_pip_packages:
|
||||
return "block"
|
||||
return RiskLevel.block.value
|
||||
|
||||
return "middle"
|
||||
return RiskLevel.middle_.value
|
||||
|
||||
@@ -13,7 +13,7 @@ This directory contains the JavaScript frontend implementation for ComfyUI-Manag
|
||||
## Sharing Components
|
||||
|
||||
- **comfyui-share-common.js**: Base functionality for workflow sharing features.
|
||||
- **comfyui-share-copus.js**: Integration with the ComfyUI Opus sharing platform.
|
||||
- **comfyui-share-copus.js**: Integration with the ComfyUI Copus sharing platform.
|
||||
- **comfyui-share-openart.js**: Integration with the OpenArt sharing platform.
|
||||
- **comfyui-share-youml.js**: Integration with the YouML sharing platform.
|
||||
|
||||
@@ -47,4 +47,4 @@ CSS files are included for specific components:
|
||||
- **custom-nodes-manager.css**: Styling for the node management UI
|
||||
- **model-manager.css**: Styling for the model management UI
|
||||
|
||||
This frontend implementation provides a comprehensive yet user-friendly interface for managing the ComfyUI ecosystem.
|
||||
This frontend implementation provides a comprehensive yet user-friendly interface for managing the ComfyUI ecosystem.
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import { $el } from "../../scripts/ui.js";
|
||||
|
||||
function normalizeContent(content) {
|
||||
const tmp = document.createElement('div');
|
||||
if (typeof content === 'string') {
|
||||
tmp.innerHTML = content;
|
||||
return Array.from(tmp.childNodes);
|
||||
}
|
||||
if (content instanceof Node) {
|
||||
return content;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export function createSettingsCombo(label, content) {
|
||||
const settingItem = $el("div.setting-item", {}, [
|
||||
$el("div.flex.flex-row.items-center.gap-2",[
|
||||
$el("div.form-label.flex.grow.items-center", [
|
||||
$el("span.text-muted", { textContent: label },)
|
||||
]),
|
||||
$el("div.form-input.flex.justify-end",
|
||||
[content]
|
||||
)
|
||||
]
|
||||
)
|
||||
]);
|
||||
return settingItem;
|
||||
}
|
||||
|
||||
export function buildGuiFrame(dialogId, title, iconClass, content, owner) {
|
||||
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
|
||||
parent: document.body,
|
||||
style: {
|
||||
position: "fixed",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
pointerEvents: "auto",
|
||||
zIndex: "1000"
|
||||
},
|
||||
onclick: (e) => {
|
||||
if (e.target === dialog_mask) {
|
||||
owner.close();
|
||||
}
|
||||
}
|
||||
// data-pc-section="mask"
|
||||
});
|
||||
|
||||
const header_actions = $el("div.p-dialog-header-actions", {
|
||||
// [TODO]
|
||||
// data-pc-section="headeractions"
|
||||
}
|
||||
);
|
||||
|
||||
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
|
||||
parent: header_actions,
|
||||
type: "button",
|
||||
ariaLabel: "Close",
|
||||
onclick: () => owner.close(),
|
||||
// "data-pc-name": "pcclosebutton",
|
||||
// "data-p-disabled": "false",
|
||||
// "data-p-severity": "secondary",
|
||||
// "data-pc-group-section": "headericon",
|
||||
// "data-pc-extend": "button",
|
||||
// "data-pc-section": "root",
|
||||
// [FIXME] Not sure how to do most of the SVG using $el
|
||||
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label"> </span><!---->'
|
||||
}
|
||||
);
|
||||
|
||||
const dialog_header = $el("div.p-dialog-header",
|
||||
[
|
||||
$el("div", [
|
||||
$el("div",
|
||||
{
|
||||
id: "frame-title-container",
|
||||
},
|
||||
[
|
||||
$el("h2.px-4", [
|
||||
$el(iconClass, {
|
||||
style: {
|
||||
"font-size": "1.25rem",
|
||||
"margin-right": ".5rem"
|
||||
}
|
||||
}),
|
||||
$el("span", { textContent: title })
|
||||
])
|
||||
]
|
||||
)
|
||||
]),
|
||||
header_actions
|
||||
]
|
||||
);
|
||||
|
||||
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
|
||||
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
|
||||
id: dialogId,
|
||||
parent: dialog_mask,
|
||||
style: {
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'pointer-events': 'auto',
|
||||
'margin': '0px',
|
||||
},
|
||||
role: 'dialog',
|
||||
ariaModal: 'true',
|
||||
// [TODO]
|
||||
// ariaLabbelledby: 'cm-title',
|
||||
// maximized: 'false',
|
||||
// data-pc-name: 'dialog',
|
||||
// data-pc-section: 'root',
|
||||
// data-pd-focustrap: 'true'
|
||||
},
|
||||
[ dialog_header, contentFrame ]
|
||||
);
|
||||
|
||||
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
|
||||
parent: manager_dialog,
|
||||
tabindex: "0",
|
||||
role: "presentation",
|
||||
ariaHidden: "true",
|
||||
"data-p-hidden-accessible": "true",
|
||||
"data-p-hidden-focusable": "true",
|
||||
"data-pc-section": "firstfocusableelement"
|
||||
});
|
||||
|
||||
return dialog_mask;
|
||||
}
|
||||
|
||||
export function buildGuiFrameCustomHeader(dialogId, customHeader, content, owner) {
|
||||
const dialog_mask = $el("div.p-dialog-mask.p-overlay-mask.p-overlay-mask-enter", {
|
||||
parent: document.body,
|
||||
style: {
|
||||
position: "fixed",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
pointerEvents: "auto",
|
||||
zIndex: "1000"
|
||||
},
|
||||
onclick: (e) => {
|
||||
if (e.target === dialog_mask) {
|
||||
owner.close();
|
||||
}
|
||||
}
|
||||
// data-pc-section="mask"
|
||||
});
|
||||
|
||||
const header_actions = $el("div.p-dialog-header-actions", {
|
||||
// [TODO]
|
||||
// data-pc-section="headeractions"
|
||||
}
|
||||
);
|
||||
|
||||
const close_button = $el("button.p-button.p-component.p-button-icon-only.p-button-secondary.p-button-rounded.p-button-text.p-dialog-close-button", {
|
||||
parent: header_actions,
|
||||
type: "button",
|
||||
ariaLabel: "Close",
|
||||
onclick: () => owner.close(),
|
||||
// "data-pc-name": "pcclosebutton",
|
||||
// "data-p-disabled": "false",
|
||||
// "data-p-severity": "secondary",
|
||||
// "data-pc-group-section": "headericon",
|
||||
// "data-pc-extend": "button",
|
||||
// "data-pc-section": "root",
|
||||
// [FIXME] Not sure how to do most of the SVG using $el
|
||||
innerHTML: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="p-icon p-button-icon" aria-hidden="true"><path d="M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z" fill="currentColor"></path></svg><span class="p-button-label" data-pc-section="label"> </span><!---->'
|
||||
}
|
||||
);
|
||||
|
||||
const _customHeader = normalizeContent(customHeader);
|
||||
const dialog_header = $el("div.p-dialog-header",
|
||||
[
|
||||
$el("div", [
|
||||
$el("div",
|
||||
{
|
||||
id: "frame-title-container",
|
||||
},
|
||||
Array.isArray(_customHeader) ? _customHeader : [_customHeader]
|
||||
)
|
||||
]),
|
||||
header_actions
|
||||
]
|
||||
);
|
||||
|
||||
const contentFrame = $el("div.p-dialog-content", {}, normalizeContent(content));
|
||||
const manager_dialog = $el("div.p-dialog.p-component.global-dialog", {
|
||||
id: dialogId,
|
||||
parent: dialog_mask,
|
||||
style: {
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'pointer-events': 'auto',
|
||||
'margin': '0px',
|
||||
},
|
||||
role: 'dialog',
|
||||
ariaModal: 'true',
|
||||
// [TODO]
|
||||
// ariaLabbelledby: 'cm-title',
|
||||
// maximized: 'false',
|
||||
// data-pc-name: 'dialog',
|
||||
// data-pc-section: 'root',
|
||||
// data-pd-focustrap: 'true'
|
||||
},
|
||||
[ dialog_header, contentFrame ]
|
||||
);
|
||||
|
||||
const hidden_accessible = $el("span.p-hidden-accessible.p-hidden-focusable", {
|
||||
parent: manager_dialog,
|
||||
tabindex: "0",
|
||||
role: "presentation",
|
||||
ariaHidden: "true",
|
||||
"data-p-hidden-accessible": "true",
|
||||
"data-p-hidden-focusable": "true",
|
||||
"data-pc-section": "firstfocusableelement"
|
||||
});
|
||||
|
||||
return dialog_mask;
|
||||
}
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
rebootAPI, setManagerInstance, show_message, customAlert, customPrompt,
|
||||
infoToast, showTerminal, setNeedRestart, generateUUID
|
||||
} from "./common.js";
|
||||
import { ComponentBuilderDialog, load_components, set_component_policy } from "./components-manager.js";
|
||||
import { CustomNodesManager } from "./custom-nodes-manager.js";
|
||||
import { ModelManager } from "./model-manager.js";
|
||||
import { SnapshotManager } from "./snapshot.js";
|
||||
import { buildGuiFrame, createSettingsCombo } from "./comfyui-gui-builder.js";
|
||||
|
||||
let manager_version = await getVersion();
|
||||
|
||||
@@ -44,12 +44,16 @@ docStyle.innerHTML = `
|
||||
|
||||
#cm-manager-dialog {
|
||||
width: 1000px;
|
||||
height: 455px;
|
||||
height: auto;
|
||||
box-sizing: content-box;
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#cm-manager-dialog br {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.cb-widget {
|
||||
width: 400px;
|
||||
height: 25px;
|
||||
@@ -80,6 +84,7 @@ docStyle.innerHTML = `
|
||||
}
|
||||
|
||||
.cm-menu-container {
|
||||
padding : calc(var(--spacing)*2);
|
||||
column-gap: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -140,8 +145,8 @@ docStyle.innerHTML = `
|
||||
}
|
||||
|
||||
.cm-notice-board {
|
||||
width: 290px;
|
||||
height: 230px;
|
||||
width: auto;
|
||||
height: 280px;
|
||||
overflow: auto;
|
||||
color: var(--input-text);
|
||||
border: 1px solid var(--descrip-text);
|
||||
@@ -222,9 +227,6 @@ function isBeforeFrontendVersion(compareVersion) {
|
||||
}
|
||||
}
|
||||
|
||||
const is_legacy_front = () => isBeforeFrontendVersion('1.2.49');
|
||||
const isNotNewManagerUI = () => isBeforeFrontendVersion('1.16.4');
|
||||
|
||||
document.head.appendChild(docStyle);
|
||||
|
||||
var update_comfyui_button = null;
|
||||
@@ -240,68 +242,50 @@ var batch_id = null;
|
||||
// copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
|
||||
const style = `
|
||||
#workflowgallery-button {
|
||||
width: 310px;
|
||||
height: 27px;
|
||||
height: 50px;
|
||||
padding: 0px !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 17px !important;
|
||||
}
|
||||
#cm-nodeinfo-button {
|
||||
width: 310px;
|
||||
height: 27px;
|
||||
padding: 0px !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 17px !important;
|
||||
|
||||
}
|
||||
#cm-manual-button {
|
||||
width: 310px;
|
||||
height: 27px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.cm-button {
|
||||
width: 310px;
|
||||
height: 30px;
|
||||
width: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 17px !important;
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
border-color: var(--border-color);
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
.cm-button:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
.cm-button-red {
|
||||
width: 310px;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 17px !important;
|
||||
background-color: #500000 !important;
|
||||
border-color: #88181b !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.cm-button-red:hover {
|
||||
background-color: #88181b !important;
|
||||
}
|
||||
|
||||
.cm-button-orange {
|
||||
width: 310px;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 17px !important;
|
||||
font-weight: bold;
|
||||
background-color: orange !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.cm-experimental-button {
|
||||
width: 290px;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 17px !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cm-experimental {
|
||||
width: 310px;
|
||||
border: 1px solid #555;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
@@ -328,8 +312,14 @@ const style = `
|
||||
|
||||
.cm-menu-combo {
|
||||
cursor: pointer;
|
||||
width: 310px;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5em 0.5em;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--comfy-menu-secondary-bg);
|
||||
}
|
||||
|
||||
.cm-menu-combo:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
.cm-small-button {
|
||||
@@ -819,7 +809,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
const isElectron = 'electronAPI' in window;
|
||||
|
||||
update_comfyui_button =
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Update ComfyUI",
|
||||
style: {
|
||||
@@ -830,7 +820,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
});
|
||||
|
||||
switch_comfyui_button =
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Switch ComfyUI",
|
||||
style: {
|
||||
@@ -841,7 +831,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
});
|
||||
|
||||
restart_stop_button =
|
||||
$el("button.cm-button-red", {
|
||||
$el("button.p-button.p-component.cm-button-red", {
|
||||
type: "button",
|
||||
textContent: "Restart",
|
||||
onclick: () => restartOrStop()
|
||||
@@ -849,7 +839,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
|
||||
if(isElectron) {
|
||||
update_all_button =
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Update All Custom Nodes",
|
||||
onclick:
|
||||
@@ -858,7 +848,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
else {
|
||||
update_all_button =
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Update All",
|
||||
onclick:
|
||||
@@ -868,7 +858,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
|
||||
const res =
|
||||
[
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Custom Nodes Manager",
|
||||
onclick:
|
||||
@@ -880,7 +870,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
}),
|
||||
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Install Missing Custom Nodes",
|
||||
onclick:
|
||||
@@ -892,7 +882,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
}),
|
||||
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Custom Nodes In Workflow",
|
||||
onclick:
|
||||
@@ -904,8 +894,8 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
}),
|
||||
|
||||
$el("br", {}, []),
|
||||
$el("button.cm-button", {
|
||||
$el("div", {}, []),
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Model Manager",
|
||||
onclick:
|
||||
@@ -917,7 +907,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
}),
|
||||
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
type: "button",
|
||||
textContent: "Install via Git URL",
|
||||
onclick: async () => {
|
||||
@@ -929,13 +919,13 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
}),
|
||||
|
||||
$el("br", {}, []),
|
||||
$el("div", {}, []),
|
||||
update_all_button,
|
||||
update_comfyui_button,
|
||||
switch_comfyui_button,
|
||||
// fetch_updates_button,
|
||||
|
||||
$el("br", {}, []),
|
||||
$el("div", {}, []),
|
||||
restart_stop_button,
|
||||
];
|
||||
|
||||
@@ -948,12 +938,13 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
let self = this;
|
||||
|
||||
// db mode
|
||||
|
||||
this.datasrc_combo = document.createElement("select");
|
||||
this.datasrc_combo.setAttribute("title", "Configure where to retrieve node/model information. If set to 'local,' the channel is ignored, and if set to 'channel (remote),' it fetches the latest information each time the list is opened.");
|
||||
this.datasrc_combo.className = "cm-menu-combo";
|
||||
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'DB: Channel (1day cache)' }, []));
|
||||
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.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled ";
|
||||
this.datasrc_combo.appendChild($el('option', { value: 'cache', text: 'Channel (1day cache)' }, []));
|
||||
this.datasrc_combo.appendChild($el('option', { value: 'local', text: 'Local' }, []));
|
||||
this.datasrc_combo.appendChild($el('option', { value: 'remote', text: 'Channel (remote)' }, []));
|
||||
|
||||
api.fetchApi('/v2/manager/db_mode')
|
||||
.then(response => response.text())
|
||||
@@ -963,27 +954,12 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
api.fetchApi(`/v2/manager/db_mode?value=${event.target.value}`);
|
||||
});
|
||||
|
||||
// preview method
|
||||
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.className = "cm-menu-combo";
|
||||
preview_combo.appendChild($el('option', { value: 'auto', text: 'Preview method: Auto' }, []));
|
||||
preview_combo.appendChild($el('option', { value: 'taesd', text: 'Preview method: TAESD (slow)' }, []));
|
||||
preview_combo.appendChild($el('option', { value: 'latent2rgb', text: 'Preview method: Latent2RGB (fast)' }, []));
|
||||
preview_combo.appendChild($el('option', { value: 'none', text: 'Preview method: None (very fast)' }, []));
|
||||
|
||||
api.fetchApi('/v2/manager/preview_method')
|
||||
.then(response => response.text())
|
||||
.then(data => { preview_combo.value = data; });
|
||||
|
||||
preview_combo.addEventListener('change', function (event) {
|
||||
api.fetchApi(`/v2/manager/preview_method?value=${event.target.value}`);
|
||||
});
|
||||
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_combo);
|
||||
|
||||
// channel
|
||||
let channel_combo = document.createElement("select");
|
||||
channel_combo.setAttribute("title", "Configure the channel for retrieving data from the Custom Node list (including missing nodes) or the Model list.");
|
||||
channel_combo.className = "cm-menu-combo";
|
||||
channel_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
|
||||
api.fetchApi('/v2/manager/channel_url_list')
|
||||
.then(response => response.json())
|
||||
.then(async data => {
|
||||
@@ -992,7 +968,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
for (let i in urls) {
|
||||
if (urls[i] != '') {
|
||||
let name_url = urls[i].split('::');
|
||||
channel_combo.appendChild($el('option', { value: name_url[0], text: `Channel: ${name_url[0]}` }, []));
|
||||
channel_combo.appendChild($el('option', { value: name_url[0], text: `${name_url[0]}` }, []));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,11 +983,13 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
});
|
||||
|
||||
const channelSetttingItem = createSettingsCombo("Channel", channel_combo);
|
||||
|
||||
|
||||
// share
|
||||
let share_combo = document.createElement("select");
|
||||
share_combo.setAttribute("title", "Hide the share button in the main menu or set the default action upon clicking it. Additionally, configure the default share site when sharing via the context menu's share button.");
|
||||
share_combo.className = "cm-menu-combo";
|
||||
share_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
|
||||
const share_options = [
|
||||
['none', 'None'],
|
||||
['openart', 'OpenArt AI'],
|
||||
@@ -1022,7 +1000,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
['all', 'All'],
|
||||
];
|
||||
for (const option of share_options) {
|
||||
share_combo.appendChild($el('option', { value: option[0], text: `Share: ${option[1]}` }, []));
|
||||
share_combo.appendChild($el('option', { value: option[0], text: `${option[1]}` }, []));
|
||||
}
|
||||
|
||||
api.fetchApi('/v2/manager/share_option')
|
||||
@@ -1044,33 +1022,14 @@ 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('/v2/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(`/v2/manager/policy/component?value=${event.target.value}`);
|
||||
set_component_policy(event.target.value);
|
||||
});
|
||||
const shareSetttingItem = createSettingsCombo("Share", share_combo);
|
||||
|
||||
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' }, []));
|
||||
update_policy_combo.className = "cm-menu-combo p-select p-component p-inputwrapper p-inputwrapper-filled";
|
||||
update_policy_combo.appendChild($el('option', { value: 'stable-comfyui', text: 'ComfyUI Stable Version' }, []));
|
||||
update_policy_combo.appendChild($el('option', { value: 'nightly-comfyui', text: 'ComfyUI Nightly Version' }, []));
|
||||
api.fetchApi('/v2/manager/policy/update')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
@@ -1081,20 +1040,20 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
api.fetchApi(`/v2/manager/policy/update?value=${event.target.value}`);
|
||||
});
|
||||
|
||||
return [
|
||||
$el("br", {}, []),
|
||||
this.datasrc_combo,
|
||||
channel_combo,
|
||||
preview_combo,
|
||||
share_combo,
|
||||
component_policy_combo,
|
||||
update_policy_combo,
|
||||
$el("br", {}, []),
|
||||
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);
|
||||
|
||||
if(isElectron)
|
||||
updateSetttingItem.style.display = 'none';
|
||||
|
||||
$el("br", {}, []),
|
||||
$el("filedset.cm-experimental", {}, [
|
||||
return [
|
||||
dbRetrievalSetttingItem,
|
||||
channelSetttingItem,
|
||||
shareSetttingItem,
|
||||
updateSetttingItem,
|
||||
//[TODO] replace mt-2 with wrapper div with flex column gap
|
||||
$el("filedset.cm-experimental.mt-auto", {}, [
|
||||
$el("legend.cm-experimental-legend", {}, ["EXPERIMENTAL"]),
|
||||
$el("button.cm-experimental-button", {
|
||||
$el("button.p-button.p-component.cm-button.cm-experimental-button", {
|
||||
type: "button",
|
||||
textContent: "Snapshot Manager",
|
||||
onclick:
|
||||
@@ -1104,7 +1063,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
SnapshotManager.instance.show();
|
||||
}
|
||||
}),
|
||||
$el("button.cm-experimental-button", {
|
||||
$el("button.p-button.p-component.cm-button.cm-experimental-button.mt-2", {
|
||||
type: "button",
|
||||
textContent: "Install PIP packages",
|
||||
onclick:
|
||||
@@ -1122,7 +1081,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
|
||||
createControlsRight() {
|
||||
const elts = [
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
id: 'cm-manual-button',
|
||||
type: "button",
|
||||
textContent: "Community Manual",
|
||||
@@ -1173,11 +1132,11 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
})
|
||||
]),
|
||||
|
||||
$el("button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
id: 'workflowgallery-button',
|
||||
type: "button",
|
||||
style: {
|
||||
...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
|
||||
// ...(localStorage.getItem("wg_last_visited") ? {height: '50px'} : {})
|
||||
},
|
||||
onclick: (e) => {
|
||||
const last_visited_site = localStorage.getItem("wg_last_visited")
|
||||
@@ -1200,7 +1159,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}, [
|
||||
$el("p", {
|
||||
id: 'workflowgallery-button-last-visited-label',
|
||||
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : ''})`,
|
||||
textContent: `(${localStorage.getItem("wg_last_visited") ? localStorage.getItem("wg_last_visited").split('/')[2] : 'none selected'})`,
|
||||
style: {
|
||||
'text-align': 'center',
|
||||
'color': 'var(--input-text)',
|
||||
@@ -1216,13 +1175,12 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
})
|
||||
]),
|
||||
|
||||
$el("button.cm-button", {
|
||||
$el("button.p-button.p-component.cm-button", {
|
||||
id: 'cm-nodeinfo-button',
|
||||
type: "button",
|
||||
textContent: "Nodes Info",
|
||||
onclick: () => { window.open("https://ltdrdata.github.io/", "comfyui-node-info"); }
|
||||
}),
|
||||
$el("br", {}, []),
|
||||
];
|
||||
|
||||
var textarea = document.createElement("div");
|
||||
@@ -1237,31 +1195,23 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => this.close() });
|
||||
const content = $el("div.cm-menu-container",
|
||||
[
|
||||
$el("div.cm-menu-column.gap-2", [...this.createControlsLeft()]),
|
||||
$el("div.cm-menu-column.gap-2", [...this.createControlsMid()]),
|
||||
$el("div.cm-menu-column.gap-2", [...this.createControlsRight()])
|
||||
]
|
||||
);
|
||||
|
||||
const content =
|
||||
$el("div.comfy-modal-content",
|
||||
[
|
||||
$el("tr.cm-title", {}, [
|
||||
$el("font", {size:6, color:"white"}, [`ComfyUI Manager ${manager_version}`])]
|
||||
),
|
||||
$el("br", {}, []),
|
||||
$el("div.cm-menu-container",
|
||||
[
|
||||
$el("div.cm-menu-column", [...this.createControlsLeft()]),
|
||||
$el("div.cm-menu-column", [...this.createControlsMid()]),
|
||||
$el("div.cm-menu-column", [...this.createControlsRight()])
|
||||
]),
|
||||
const frame = buildGuiFrame(
|
||||
'cm-manager-dialog', // dialog id
|
||||
`ComfyUI Manager ${manager_version}`, // dialog title
|
||||
"i.mdi.mdi-puzzle", // dialog icon class to show before title
|
||||
content, // dialog content element
|
||||
this
|
||||
); // send this so we can attach close functions
|
||||
|
||||
$el("br", {}, []),
|
||||
close_button,
|
||||
]
|
||||
);
|
||||
|
||||
content.style.width = '100%';
|
||||
content.style.height = '100%';
|
||||
|
||||
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
|
||||
this.element = frame;
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
@@ -1269,7 +1219,7 @@ class ManagerMenuDialog extends ComfyDialog {
|
||||
}
|
||||
|
||||
show() {
|
||||
this.element.style.display = "block";
|
||||
this.element.style.display = "flex";
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
@@ -1438,14 +1388,6 @@ app.registerExtension({
|
||||
});
|
||||
},
|
||||
async setup() {
|
||||
let orig_clear = app.graph.clear;
|
||||
app.graph.clear = function () {
|
||||
orig_clear.call(app.graph);
|
||||
load_components();
|
||||
};
|
||||
|
||||
load_components();
|
||||
|
||||
const menu = document.querySelector(".comfy-menu");
|
||||
const separator = document.createElement("hr");
|
||||
|
||||
@@ -1517,11 +1459,6 @@ app.registerExtension({
|
||||
tooltip: "Share"
|
||||
}).element
|
||||
);
|
||||
|
||||
const shouldShowLegacyMenuItems = isNotNewManagerUI();
|
||||
if (shouldShowLegacyMenuItems) {
|
||||
app.menu?.settingsGroup.element.before(cmGroup.element);
|
||||
}
|
||||
}
|
||||
catch(exception) {
|
||||
console.log('ComfyUI is outdated. New style menu based features are disabled.');
|
||||
@@ -1579,19 +1516,6 @@ app.registerExtension({
|
||||
node.prototype.getExtraMenuOptions = function (_, options) {
|
||||
origGetExtraMenuOptions?.apply?.(this, arguments);
|
||||
|
||||
if (node.category.startsWith('group nodes>')) {
|
||||
options.push({
|
||||
content: "Save As Component",
|
||||
callback: (obj) => {
|
||||
if (!ComponentBuilderDialog.instance) {
|
||||
ComponentBuilderDialog.instance = new ComponentBuilderDialog();
|
||||
}
|
||||
ComponentBuilderDialog.instance.target_node = node;
|
||||
ComponentBuilderDialog.instance.show();
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
if (isOutputNode(node)) {
|
||||
const { potential_outputs } = getPotentialOutputsAndOutputNodes([this]);
|
||||
const hasOutput = potential_outputs.length > 0;
|
||||
|
||||
@@ -552,6 +552,20 @@ export class ShareDialog extends ComfyDialog {
|
||||
this.matrix_destination_checkbox.style.color = "var(--fg-color)";
|
||||
this.matrix_destination_checkbox.checked = this.share_option === 'matrix'; //true;
|
||||
|
||||
try {
|
||||
api.fetchApi(`/v2/manager/get_matrix_dep_status`)
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if(data == 'unavailable') {
|
||||
matrix_destination_checkbox_text.style.textDecoration = "line-through";
|
||||
this.matrix_destination_checkbox.disabled = true;
|
||||
this.matrix_destination_checkbox.title = "It has been disabled because the 'matrix-nio' dependency is not installed. Please install this dependency to use the matrix sharing feature.";
|
||||
matrix_destination_checkbox_text.title = "It has been disabled because the 'matrix-nio' dependency is not installed. Please install this dependency to use the matrix sharing feature.";
|
||||
}
|
||||
})
|
||||
.catch(error => {});
|
||||
} catch (error) {}
|
||||
|
||||
this.comfyworkflows_destination_checkbox = $el("input", { type: 'checkbox', id: "comfyworkflows_destination" }, [])
|
||||
const comfyworkflows_destination_checkbox_text = $el("label", {}, [" ComfyWorkflows.com"])
|
||||
this.comfyworkflows_destination_checkbox.style.color = "var(--fg-color)";
|
||||
|
||||
@@ -71,7 +71,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
this.allFiles = [];
|
||||
this.titleNum = 0;
|
||||
}
|
||||
|
||||
|
||||
createButtons() {
|
||||
const inputStyle = {
|
||||
display: "block",
|
||||
@@ -201,13 +201,15 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
});
|
||||
this.LockInput = $el("input", {
|
||||
type: "text",
|
||||
placeholder: "",
|
||||
style: {
|
||||
placeholder: "0",
|
||||
style: {
|
||||
width: "100px",
|
||||
padding: "7px",
|
||||
paddingLeft: "30px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #ddd",
|
||||
boxSizing: "border-box",
|
||||
position: "relative",
|
||||
},
|
||||
oninput: (event) => {
|
||||
let input = event.target.value;
|
||||
@@ -301,7 +303,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
|
||||
const titleNumDom = $el(
|
||||
"label",
|
||||
{
|
||||
@@ -342,15 +344,11 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
["0/70"]
|
||||
);
|
||||
// Additional Inputs Section
|
||||
const additionalInputsSection = $el(
|
||||
"div",
|
||||
{ style: { ...sectionStyle, } },
|
||||
[
|
||||
$el("label", { style: labelStyle }, ["3️⃣ Title "]),
|
||||
this.TitleInput,
|
||||
titleNumDom,
|
||||
]
|
||||
);
|
||||
const additionalInputsSection = $el("div", { style: { ...sectionStyle } }, [
|
||||
$el("label", { style: labelStyle }, ["3️⃣ Title "]),
|
||||
this.TitleInput,
|
||||
titleNumDom,
|
||||
]);
|
||||
const SubtitleSection = $el("div", { style: sectionStyle }, [
|
||||
$el("label", { style: labelStyle }, ["4️⃣ Subtitle "]),
|
||||
this.SubTitleInput,
|
||||
@@ -379,7 +377,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
});
|
||||
|
||||
const blockChainSection_lock = $el("div", { style: sectionStyle }, [
|
||||
$el("label", { style: labelStyle }, ["6️⃣ Pay to download"]),
|
||||
$el("label", { style: labelStyle }, ["6️⃣ Download threshold"]),
|
||||
$el(
|
||||
"label",
|
||||
{
|
||||
@@ -392,11 +390,42 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
},
|
||||
[
|
||||
this.radioButtonsCheck_lock,
|
||||
$el("div", { style: { marginLeft: "5px" ,display:'flex',alignItems:'center'} }, [
|
||||
$el("span", { style: { marginLeft: "5px" } }, ["ON"]),
|
||||
$el("span", { style: { marginLeft: "20px",marginRight:'10px' ,color:'#fff'} }, ["Price US$"]),
|
||||
this.LockInput
|
||||
]),
|
||||
$el(
|
||||
"div",
|
||||
{
|
||||
style: {
|
||||
marginLeft: "5px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
},
|
||||
},
|
||||
[
|
||||
$el("span", { style: { marginLeft: "5px" } }, ["ON"]),
|
||||
$el(
|
||||
"span",
|
||||
{
|
||||
style: {
|
||||
marginLeft: "20px",
|
||||
marginRight: "10px",
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
["Unlock with"]
|
||||
),
|
||||
$el("img", {
|
||||
style: {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
position: "absolute",
|
||||
right: "75px",
|
||||
zIndex: "100",
|
||||
},
|
||||
src: "https://static.copus.io/images/admin/202507/prod/e2919a1d8f3c2d99d3b8fe27ff94b841.png",
|
||||
}),
|
||||
this.LockInput,
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
$el(
|
||||
@@ -404,14 +433,25 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
|
||||
[
|
||||
this.radioButtonsCheckOff_lock,
|
||||
$el("span", { style: { marginLeft: "5px" } }, ["OFF"]),
|
||||
$el(
|
||||
"div",
|
||||
{
|
||||
style: {
|
||||
marginLeft: "5px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
},
|
||||
[$el("span", { style: { marginLeft: "5px" } }, ["OFF"])]
|
||||
),
|
||||
]
|
||||
),
|
||||
|
||||
|
||||
$el(
|
||||
"p",
|
||||
{ style: { fontSize: "16px", color: "#fff", margin: "10px 0 0 0" } },
|
||||
["Get paid from your workflow. You can change the price and withdraw your earnings on Copus."]
|
||||
[
|
||||
]
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -432,7 +472,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
});
|
||||
|
||||
const blockChainSection = $el("div", { style: sectionStyle }, [
|
||||
$el("label", { style: labelStyle }, ["7️⃣ Store on blockchain "]),
|
||||
$el("label", { style: labelStyle }, ["8️⃣ Store on blockchain "]),
|
||||
$el(
|
||||
"label",
|
||||
{
|
||||
@@ -463,6 +503,139 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
),
|
||||
]);
|
||||
|
||||
this.ratingRadioButtonsCheck0 = $el("input", {
|
||||
type: "radio",
|
||||
name: "content_rating",
|
||||
value: "0",
|
||||
id: "content_rating0",
|
||||
});
|
||||
this.ratingRadioButtonsCheck1 = $el("input", {
|
||||
type: "radio",
|
||||
name: "content_rating",
|
||||
value: "1",
|
||||
id: "content_rating1",
|
||||
});
|
||||
this.ratingRadioButtonsCheck2 = $el("input", {
|
||||
type: "radio",
|
||||
name: "content_rating",
|
||||
value: "2",
|
||||
id: "content_rating2",
|
||||
});
|
||||
this.ratingRadioButtonsCheck_1 = $el("input", {
|
||||
type: "radio",
|
||||
name: "content_rating",
|
||||
value: "-1",
|
||||
id: "content_rating_1",
|
||||
checked: true,
|
||||
});
|
||||
|
||||
// content rating
|
||||
const contentRatingSection = $el("div", { style: sectionStyle }, [
|
||||
$el("label", { style: labelStyle }, ["7️⃣ Content rating "]),
|
||||
$el(
|
||||
"label",
|
||||
{
|
||||
style: {
|
||||
marginTop: "10px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
},
|
||||
},
|
||||
[
|
||||
this.ratingRadioButtonsCheck0,
|
||||
$el("img", {
|
||||
style: {
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
marginLeft: "5px",
|
||||
},
|
||||
src: "https://static.copus.io/images/client/202507/test/b9f17da83b054d53cd0cb4508c2c30dc.png",
|
||||
}),
|
||||
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
|
||||
"All ages",
|
||||
]),
|
||||
]
|
||||
),
|
||||
$el(
|
||||
"p",
|
||||
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
|
||||
["Safe for all viewers; no profanity, violence, or mature themes."]
|
||||
),
|
||||
$el(
|
||||
"label",
|
||||
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
|
||||
[
|
||||
this.ratingRadioButtonsCheck1,
|
||||
$el("img", {
|
||||
style: {
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
marginLeft: "5px",
|
||||
},
|
||||
src: "https://static.copus.io/images/client/202507/test/7848bc0d3690671df21c7cf00c4cfc81.png",
|
||||
}),
|
||||
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
|
||||
"13+ (Teen)",
|
||||
]),
|
||||
]
|
||||
),
|
||||
$el(
|
||||
"p",
|
||||
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
|
||||
[
|
||||
"Mild language, light themes, or cartoon violence; no explicit content. ",
|
||||
]
|
||||
),
|
||||
$el(
|
||||
"label",
|
||||
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
|
||||
[
|
||||
this.ratingRadioButtonsCheck2,
|
||||
$el("img", {
|
||||
style: {
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
marginLeft: "5px",
|
||||
},
|
||||
src: "https://static.copus.io/images/client/202507/test/bc51839c208d68d91173e43c23bff039.png",
|
||||
}),
|
||||
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
|
||||
"18+ (Explicit)",
|
||||
]),
|
||||
]
|
||||
),
|
||||
$el(
|
||||
"p",
|
||||
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
|
||||
[
|
||||
"Explicit content, including sexual content, strong violence, or intense themes. ",
|
||||
]
|
||||
),
|
||||
$el(
|
||||
"label",
|
||||
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
|
||||
[
|
||||
this.ratingRadioButtonsCheck_1,
|
||||
$el("img", {
|
||||
style: {
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
marginLeft: "5px",
|
||||
},
|
||||
src: "https://static.copus.io/images/client/202507/test/5c802fdcaaea4e7bbed37393eec0d5ba.png",
|
||||
}),
|
||||
$el("span", { style: { marginLeft: "5px", color: "#fff" } }, [
|
||||
"Not Rated",
|
||||
]),
|
||||
]
|
||||
),
|
||||
$el(
|
||||
"p",
|
||||
{ style: { fontSize: "10px", color: "#fff", marginLeft: "20px" } },
|
||||
["No age rating provided."]
|
||||
),
|
||||
]);
|
||||
|
||||
// Message Section
|
||||
this.message = $el(
|
||||
@@ -526,6 +699,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
DescriptionSection,
|
||||
// contestSection,
|
||||
blockChainSection_lock,
|
||||
contentRatingSection,
|
||||
blockChainSection,
|
||||
this.message,
|
||||
buttonsSection,
|
||||
@@ -534,7 +708,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
return layout;
|
||||
}
|
||||
/**
|
||||
* api
|
||||
* api
|
||||
* @param {url} path
|
||||
* @param {params} options
|
||||
* @param {statusText} statusText
|
||||
@@ -587,7 +761,9 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
url: data,
|
||||
});
|
||||
} else {
|
||||
throw new Error("make sure your API key is correct and try again later");
|
||||
throw new Error(
|
||||
"make sure your API key is correct and try again later"
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e?.response?.status === 413) {
|
||||
@@ -628,8 +804,15 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
subTitle: this.SubTitleInput.value,
|
||||
content: this.descriptionInput.value,
|
||||
storeOnChain: this.radioButtonsCheck.checked ? true : false,
|
||||
lockState:this.radioButtonsCheck_lock.checked ? 2 : 0,
|
||||
unlockPrice:this.LockInput.value,
|
||||
lockState: this.radioButtonsCheck_lock.checked ? 2 : 0,
|
||||
unlockPrice: this.LockInput.value,
|
||||
rating: this.ratingRadioButtonsCheck0.checked
|
||||
? 0
|
||||
: this.ratingRadioButtonsCheck1.checked
|
||||
? 1
|
||||
: this.ratingRadioButtonsCheck2.checked
|
||||
? 2
|
||||
: -1,
|
||||
};
|
||||
|
||||
if (!this.keyInput.value) {
|
||||
@@ -644,8 +827,8 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
throw new Error("Title is required");
|
||||
}
|
||||
|
||||
if(this.radioButtonsCheck_lock.checked){
|
||||
if (!this.LockInput.value){
|
||||
if (this.radioButtonsCheck_lock.checked) {
|
||||
if (!this.LockInput.value) {
|
||||
throw new Error("Price is required");
|
||||
}
|
||||
}
|
||||
@@ -695,23 +878,23 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
"Uploading workflow..."
|
||||
);
|
||||
|
||||
if (res.status && res.data.status && res.data) {
|
||||
localStorage.setItem("copus_token",this.keyInput.value);
|
||||
const { data } = res.data;
|
||||
if (data) {
|
||||
const url = `${DEFAULT_HOMEPAGE_URL}/work/${data}`;
|
||||
this.message.innerHTML = `Workflow has been shared successfully. <a href="${url}" target="_blank">Click here to view it.</a>`;
|
||||
this.previewImage.src = "";
|
||||
this.previewImage.style.display = "none";
|
||||
this.uploadedImages = [];
|
||||
this.allFilesImages = [];
|
||||
this.allFiles = [];
|
||||
this.TitleInput.value = "";
|
||||
this.SubTitleInput.value = "";
|
||||
this.descriptionInput.value = "";
|
||||
this.selectedFile = null;
|
||||
}
|
||||
}
|
||||
if (res.status && res.data.status && res.data) {
|
||||
localStorage.setItem("copus_token", this.keyInput.value);
|
||||
const { data } = res.data;
|
||||
if (data) {
|
||||
const url = `${DEFAULT_HOMEPAGE_URL}/work/${data}`;
|
||||
this.message.innerHTML = `Workflow has been shared successfully. <a href="${url}" target="_blank">Click here to view it.</a>`;
|
||||
this.previewImage.src = "";
|
||||
this.previewImage.style.display = "none";
|
||||
this.uploadedImages = [];
|
||||
this.allFilesImages = [];
|
||||
this.allFiles = [];
|
||||
this.TitleInput.value = "";
|
||||
this.SubTitleInput.value = "";
|
||||
this.descriptionInput.value = "";
|
||||
this.selectedFile = null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Error sharing workflow: " + e.message);
|
||||
}
|
||||
@@ -757,7 +940,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
this.element.style.display = "block";
|
||||
this.previewImage.src = "";
|
||||
this.previewImage.style.display = "none";
|
||||
this.keyInput.value = apiToken!=null?apiToken:"";
|
||||
this.keyInput.value = apiToken != null ? apiToken : "";
|
||||
this.uploadedImages = [];
|
||||
this.allFilesImages = [];
|
||||
this.allFiles = [];
|
||||
|
||||
@@ -1,812 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { sleep, show_message, customConfirm, customAlert } from "./common.js";
|
||||
import { GroupNodeConfig, GroupNodeHandler } from "../../extensions/core/groupNode.js";
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
|
||||
const SEPARATOR = ">"
|
||||
|
||||
let pack_map = {};
|
||||
let rpack_map = {};
|
||||
|
||||
export function getPureName(node) {
|
||||
// group nodes/
|
||||
let category = null;
|
||||
if(node.category) {
|
||||
category = node.category.substring(12);
|
||||
}
|
||||
else {
|
||||
category = node.constructor.category?.substring(12);
|
||||
}
|
||||
if(category) {
|
||||
let purename = node.comfyClass.substring(category.length+1);
|
||||
return purename;
|
||||
}
|
||||
else if(node.comfyClass.startsWith('workflow/') || node.comfyClass.startsWith(`workflow${SEPARATOR}`)) {
|
||||
return node.comfyClass.substring(9);
|
||||
}
|
||||
else {
|
||||
return node.comfyClass;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidVersionString(version) {
|
||||
const versionPattern = /^(\d+)\.(\d+)(\.(\d+))?$/;
|
||||
|
||||
const match = version.match(versionPattern);
|
||||
|
||||
return match !== null &&
|
||||
parseInt(match[1], 10) >= 0 &&
|
||||
parseInt(match[2], 10) >= 0 &&
|
||||
(!match[3] || parseInt(match[4], 10) >= 0);
|
||||
}
|
||||
|
||||
function register_pack_map(name, data) {
|
||||
if(data.packname) {
|
||||
pack_map[data.packname] = name;
|
||||
rpack_map[name] = data;
|
||||
}
|
||||
else {
|
||||
rpack_map[name] = data;
|
||||
}
|
||||
}
|
||||
|
||||
function storeGroupNode(name, data, register=true) {
|
||||
let extra = app.graph.extra;
|
||||
if (!extra) app.graph.extra = extra = {};
|
||||
let groupNodes = extra.groupNodes;
|
||||
if (!groupNodes) extra.groupNodes = groupNodes = {};
|
||||
groupNodes[name] = data;
|
||||
|
||||
if(register) {
|
||||
register_pack_map(name, data);
|
||||
}
|
||||
}
|
||||
|
||||
export async function load_components() {
|
||||
let data = await api.fetchApi('/v2/manager/component/loads', {method: "POST"});
|
||||
let components = await data.json();
|
||||
|
||||
let start_time = Date.now();
|
||||
let failed = [];
|
||||
let failed2 = [];
|
||||
|
||||
for(let name in components) {
|
||||
if(app.graph.extra?.groupNodes?.[name]) {
|
||||
if(data) {
|
||||
let data = components[name];
|
||||
|
||||
let category = data.packname;
|
||||
if(data.category) {
|
||||
category += SEPARATOR + data.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
const config = new GroupNodeConfig(name, data);
|
||||
await config.registerType(category);
|
||||
|
||||
register_pack_map(name, data);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let nodeData = components[name];
|
||||
|
||||
storeGroupNode(name, nodeData);
|
||||
|
||||
const config = new GroupNodeConfig(name, nodeData);
|
||||
|
||||
while(true) {
|
||||
try {
|
||||
let category = nodeData.packname;
|
||||
if(nodeData.category) {
|
||||
category += SEPARATOR + nodeData.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
await config.registerType(category);
|
||||
register_pack_map(name, nodeData);
|
||||
break;
|
||||
}
|
||||
catch {
|
||||
let elapsed_time = Date.now() - start_time;
|
||||
if (elapsed_time > 5000) {
|
||||
failed.push(name);
|
||||
break;
|
||||
} else {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback1
|
||||
for(let i in failed) {
|
||||
let name = failed[i];
|
||||
|
||||
if(app.graph.extra?.groupNodes?.[name]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let nodeData = components[name];
|
||||
|
||||
storeGroupNode(name, nodeData);
|
||||
|
||||
const config = new GroupNodeConfig(name, nodeData);
|
||||
while(true) {
|
||||
try {
|
||||
let category = nodeData.packname;
|
||||
if(nodeData.workflow.category) {
|
||||
category += SEPARATOR + nodeData.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
await config.registerType(category);
|
||||
register_pack_map(name, nodeData);
|
||||
break;
|
||||
}
|
||||
catch {
|
||||
let elapsed_time = Date.now() - start_time;
|
||||
if (elapsed_time > 10000) {
|
||||
failed2.push(name);
|
||||
break;
|
||||
} else {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback2
|
||||
for(let name in failed2) {
|
||||
let name = failed2[i];
|
||||
|
||||
let nodeData = components[name];
|
||||
|
||||
storeGroupNode(name, nodeData);
|
||||
|
||||
const config = new GroupNodeConfig(name, nodeData);
|
||||
while(true) {
|
||||
try {
|
||||
let category = nodeData.workflow.packname;
|
||||
if(nodeData.workflow.category) {
|
||||
category += SEPARATOR + nodeData.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
await config.registerType(category);
|
||||
register_pack_map(name, nodeData);
|
||||
break;
|
||||
}
|
||||
catch {
|
||||
let elapsed_time = Date.now() - start_time;
|
||||
if (elapsed_time > 30000) {
|
||||
failed.push(name);
|
||||
break;
|
||||
} else {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function save_as_component(node, version, author, prefix, nodename, packname, category) {
|
||||
let component_name = `${prefix}::${nodename}`;
|
||||
|
||||
let subgraph = app.graph.extra?.groupNodes?.[component_name];
|
||||
if(!subgraph) {
|
||||
subgraph = app.graph.extra?.groupNodes?.[getPureName(node)];
|
||||
}
|
||||
|
||||
subgraph.version = version;
|
||||
subgraph.author = author;
|
||||
subgraph.datetime = Date.now();
|
||||
subgraph.packname = packname;
|
||||
subgraph.category = category;
|
||||
|
||||
let body =
|
||||
{
|
||||
name: component_name,
|
||||
workflow: subgraph
|
||||
};
|
||||
|
||||
pack_map[packname] = component_name;
|
||||
rpack_map[component_name] = subgraph;
|
||||
|
||||
const res = await api.fetchApi('/v2/manager/component/save', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if(res.status == 200) {
|
||||
storeGroupNode(component_name, subgraph);
|
||||
const config = new GroupNodeConfig(component_name, subgraph);
|
||||
|
||||
let category = body.workflow.packname;
|
||||
if(body.workflow.category) {
|
||||
category += SEPARATOR + body.workflow.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
await config.registerType(category);
|
||||
|
||||
let path = await res.text();
|
||||
show_message(`Component '${component_name}' is saved into:\n${path}`);
|
||||
}
|
||||
else
|
||||
show_message(`Failed to save component.`);
|
||||
}
|
||||
|
||||
async function import_component(component_name, component, mode) {
|
||||
if(mode) {
|
||||
let body =
|
||||
{
|
||||
name: component_name,
|
||||
workflow: component
|
||||
};
|
||||
|
||||
const res = await api.fetchApi('/v2/manager/component/save', {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
let category = component.packname;
|
||||
if(component.category) {
|
||||
category += SEPARATOR + component.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
}
|
||||
|
||||
storeGroupNode(component_name, component);
|
||||
const config = new GroupNodeConfig(component_name, component);
|
||||
await config.registerType(category);
|
||||
}
|
||||
|
||||
function restore_to_loaded_component(component_name) {
|
||||
if(rpack_map[component_name]) {
|
||||
let component = rpack_map[component_name];
|
||||
storeGroupNode(component_name, component, false);
|
||||
const config = new GroupNodeConfig(component_name, component);
|
||||
config.registerType(component.category);
|
||||
}
|
||||
}
|
||||
|
||||
// Using a timestamp prevents duplicate pastes and ensures the prevention of re-deletion of litegrapheditor_clipboard.
|
||||
let last_paste_timestamp = null;
|
||||
|
||||
function versionCompare(v1, v2) {
|
||||
let ver1;
|
||||
let ver2;
|
||||
if(v1 && v1 != '') {
|
||||
ver1 = v1.split('.');
|
||||
ver1[0] = parseInt(ver1[0]);
|
||||
ver1[1] = parseInt(ver1[1]);
|
||||
if(ver1.length == 2)
|
||||
ver1.push(0);
|
||||
else
|
||||
ver1[2] = parseInt(ver2[2]);
|
||||
}
|
||||
else {
|
||||
ver1 = [0,0,0];
|
||||
}
|
||||
|
||||
if(v2 && v2 != '') {
|
||||
ver2 = v2.split('.');
|
||||
ver2[0] = parseInt(ver2[0]);
|
||||
ver2[1] = parseInt(ver2[1]);
|
||||
if(ver2.length == 2)
|
||||
ver2.push(0);
|
||||
else
|
||||
ver2[2] = parseInt(ver2[2]);
|
||||
}
|
||||
else {
|
||||
ver2 = [0,0,0];
|
||||
}
|
||||
|
||||
if(ver1[0] > ver2[0])
|
||||
return -1;
|
||||
else if(ver1[0] < ver2[0])
|
||||
return 1;
|
||||
|
||||
if(ver1[1] > ver2[1])
|
||||
return -1;
|
||||
else if(ver1[1] < ver2[1])
|
||||
return 1;
|
||||
|
||||
if(ver1[2] > ver2[2])
|
||||
return -1;
|
||||
else if(ver1[2] < ver2[2])
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function checkVersion(name, component) {
|
||||
let msg = '';
|
||||
if(rpack_map[name]) {
|
||||
let old_version = rpack_map[name].version;
|
||||
if(!old_version || old_version == '') {
|
||||
msg = ` '${name}' Upgrade (V0.0 -> V${component.version})`;
|
||||
}
|
||||
else {
|
||||
let c = versionCompare(old_version, component.version);
|
||||
if(c < 0) {
|
||||
msg = ` '${name}' Downgrade (V${old_version} -> V${component.version})`;
|
||||
}
|
||||
else if(c > 0) {
|
||||
msg = ` '${name}' Upgrade (V${old_version} -> V${component.version})`;
|
||||
}
|
||||
else {
|
||||
msg = ` '${name}' Same version (V${component.version})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
msg = `'${name}' NEW (V${component.version})`;
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
async function handle_import_components(components) {
|
||||
let msg = 'Components:\n';
|
||||
let cnt = 0;
|
||||
for(let name in components) {
|
||||
let component = components[name];
|
||||
let v = checkVersion(name, component);
|
||||
|
||||
if(cnt < 10) {
|
||||
msg += v + '\n';
|
||||
}
|
||||
else if (cnt == 10) {
|
||||
msg += '...\n';
|
||||
}
|
||||
else {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
cnt++;
|
||||
}
|
||||
|
||||
let last_name = null;
|
||||
msg += '\nWill you load components?\n';
|
||||
const confirmed = await customConfirm(msg);
|
||||
if(confirmed) {
|
||||
const mode = await customConfirm('\nWill you save components?\n(cancel=load without save)');
|
||||
|
||||
for(let name in components) {
|
||||
let component = components[name];
|
||||
import_component(name, component, mode);
|
||||
last_name = name;
|
||||
}
|
||||
|
||||
if(mode) {
|
||||
show_message('Components are saved.');
|
||||
}
|
||||
else {
|
||||
show_message('Components are loaded.');
|
||||
}
|
||||
}
|
||||
|
||||
if(cnt == 1 && last_name) {
|
||||
const node = LiteGraph.createNode(`workflow${SEPARATOR}${last_name}`);
|
||||
node.pos = [app.canvas.graph_mouse[0], app.canvas.graph_mouse[1]];
|
||||
app.canvas.graph.add(node, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePaste(e) {
|
||||
let data = (e.clipboardData || window.clipboardData);
|
||||
const items = data.items;
|
||||
for(const item of items) {
|
||||
if(item.kind == 'string' && item.type == 'text/plain') {
|
||||
data = data.getData("text/plain");
|
||||
try {
|
||||
let json_data = JSON.parse(data);
|
||||
if(json_data.kind == 'ComfyUI Components' && last_paste_timestamp != json_data.timestamp) {
|
||||
last_paste_timestamp = json_data.timestamp;
|
||||
await handle_import_components(json_data.components);
|
||||
|
||||
// disable paste node
|
||||
localStorage.removeItem("litegrapheditor_clipboard", null);
|
||||
}
|
||||
else {
|
||||
console.log('This components are already pasted: ignored');
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("paste", handlePaste);
|
||||
|
||||
|
||||
export class ComponentBuilderDialog extends ComfyDialog {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
clear() {
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
this.invalidateControl();
|
||||
|
||||
this.element.style.display = "block";
|
||||
this.element.style.zIndex = 1099;
|
||||
this.element.style.width = "500px";
|
||||
this.element.style.height = "480px";
|
||||
}
|
||||
|
||||
invalidateControl() {
|
||||
this.clear();
|
||||
|
||||
let self = this;
|
||||
|
||||
const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => self.close() });
|
||||
this.save_button = $el("button",
|
||||
{ id: "cm-save-button", type: "button", textContent: "Save", onclick: () =>
|
||||
{
|
||||
save_as_component(self.target_node, self.version_string.value.trim(), self.author.value.trim(), self.node_prefix.value.trim(),
|
||||
self.getNodeName(), self.getPackName(), self.category.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
let default_nodename = getPureName(this.target_node).trim();
|
||||
|
||||
let groupNode = app.graph.extra.groupNodes[default_nodename];
|
||||
let default_packname = groupNode.packname;
|
||||
if(!default_packname) {
|
||||
default_packname = '';
|
||||
}
|
||||
|
||||
let default_category = groupNode.category;
|
||||
if(!default_category) {
|
||||
default_category = '';
|
||||
}
|
||||
|
||||
this.default_ver = groupNode.version;
|
||||
if(!this.default_ver) {
|
||||
this.default_ver = '0.0';
|
||||
}
|
||||
|
||||
let default_author = groupNode.author;
|
||||
if(!default_author) {
|
||||
default_author = '';
|
||||
}
|
||||
|
||||
let delimiterIndex = default_nodename.indexOf('::');
|
||||
let default_prefix = "";
|
||||
if(delimiterIndex != -1) {
|
||||
default_prefix = default_nodename.substring(0, delimiterIndex);
|
||||
default_nodename = default_nodename.substring(delimiterIndex + 2);
|
||||
}
|
||||
|
||||
if(!default_prefix) {
|
||||
this.save_button.disabled = true;
|
||||
}
|
||||
|
||||
this.pack_list = this.createPackListCombo();
|
||||
|
||||
let version_string = this.createLabeledInput('input version (e.g. 1.0)', '*Version : ', this.default_ver);
|
||||
this.version_string = version_string[1];
|
||||
this.version_string.disabled = true;
|
||||
|
||||
let author = this.createLabeledInput('input author (e.g. Dr.Lt.Data)', 'Author : ', default_author);
|
||||
this.author = author[1];
|
||||
|
||||
let node_prefix = this.createLabeledInput('input node prefix (e.g. mypack)', '*Prefix : ', default_prefix);
|
||||
this.node_prefix = node_prefix[1];
|
||||
|
||||
let manual_nodename = this.createLabeledInput('input node name (e.g. MAKE_BASIC_PIPE)', 'Nodename : ', default_nodename);
|
||||
this.manual_nodename = manual_nodename[1];
|
||||
|
||||
let manual_packname = this.createLabeledInput('input pack name (e.g. mypack)', 'Packname : ', default_packname);
|
||||
this.manual_packname = manual_packname[1];
|
||||
|
||||
let category = this.createLabeledInput('input category (e.g. util/pipe)', 'Category : ', default_category);
|
||||
this.category = category[1];
|
||||
|
||||
this.node_label = this.createNodeLabel();
|
||||
|
||||
let author_mode = this.createAuthorModeCheck();
|
||||
this.author_mode = author_mode[0];
|
||||
|
||||
const content =
|
||||
$el("div.comfy-modal-content",
|
||||
[
|
||||
$el("tr.cm-title", {}, [
|
||||
$el("font", {size:6, color:"white"}, [`ComfyUI-Manager: Component Builder`])]
|
||||
),
|
||||
$el("br", {}, []),
|
||||
$el("div.cm-menu-container",
|
||||
[
|
||||
author_mode[0],
|
||||
author_mode[1],
|
||||
category[0],
|
||||
author[0],
|
||||
node_prefix[0],
|
||||
manual_nodename[0],
|
||||
manual_packname[0],
|
||||
version_string[0],
|
||||
this.pack_list,
|
||||
$el("br", {}, []),
|
||||
this.node_label
|
||||
]),
|
||||
|
||||
$el("br", {}, []),
|
||||
this.save_button,
|
||||
close_button,
|
||||
]
|
||||
);
|
||||
|
||||
content.style.width = '100%';
|
||||
content.style.height = '100%';
|
||||
|
||||
this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]);
|
||||
}
|
||||
|
||||
validateInput() {
|
||||
let msg = "";
|
||||
|
||||
if(!isValidVersionString(this.version_string.value)) {
|
||||
msg += 'Invalid version string: '+event.value+"\n";
|
||||
}
|
||||
|
||||
if(this.node_prefix.value.trim() == '') {
|
||||
msg += 'Node prefix cannot be empty\n';
|
||||
}
|
||||
|
||||
if(this.manual_nodename.value.trim() == '') {
|
||||
msg += 'Node name cannot be empty\n';
|
||||
}
|
||||
|
||||
if(msg != '') {
|
||||
// alert(msg);
|
||||
}
|
||||
|
||||
this.save_button.disabled = msg != "";
|
||||
}
|
||||
|
||||
getPackName() {
|
||||
if(this.pack_list.selectedIndex == 0) {
|
||||
return this.manual_packname.value.trim();
|
||||
}
|
||||
|
||||
return this.pack_list.value.trim();
|
||||
}
|
||||
|
||||
getNodeName() {
|
||||
if(this.manual_nodename.value.trim() != '') {
|
||||
return this.manual_nodename.value.trim();
|
||||
}
|
||||
|
||||
return getPureName(this.target_node);
|
||||
}
|
||||
|
||||
createAuthorModeCheck() {
|
||||
let check = $el("input",{type:'checkbox', id:"author-mode"},[])
|
||||
const check_label = $el("label",{for:"author-mode"},["Enable author mode"]);
|
||||
check_label.style.color = "var(--fg-color)";
|
||||
check_label.style.cursor = "pointer";
|
||||
check.checked = false;
|
||||
|
||||
let self = this;
|
||||
check.onchange = () => {
|
||||
self.version_string.disabled = !check.checked;
|
||||
|
||||
if(!check.checked) {
|
||||
self.version_string.value = self.default_ver;
|
||||
}
|
||||
else {
|
||||
customAlert('If you are not the author, it is not recommended to change the version, as it may cause component update issues.');
|
||||
}
|
||||
};
|
||||
|
||||
return [check, check_label];
|
||||
}
|
||||
|
||||
createNodeLabel() {
|
||||
let label = $el('p');
|
||||
label.className = 'cb-node-label';
|
||||
if(this.target_node.comfyClass.includes('::'))
|
||||
label.textContent = getPureName(this.target_node);
|
||||
else
|
||||
label.textContent = " _::" + getPureName(this.target_node);
|
||||
return label;
|
||||
}
|
||||
|
||||
createLabeledInput(placeholder, label, value) {
|
||||
let textbox = $el('input.cb-widget-input', {type:'text', placeholder:placeholder, value:value}, []);
|
||||
|
||||
let self = this;
|
||||
textbox.onchange = () => {
|
||||
this.validateInput.call(self);
|
||||
this.node_label.textContent = this.node_prefix.value + "::" + this.manual_nodename.value;
|
||||
}
|
||||
let row = $el('span.cb-widget', {}, [ $el('span.cb-widget-input-label', label), textbox]);
|
||||
|
||||
return [row, textbox];
|
||||
}
|
||||
|
||||
createPackListCombo() {
|
||||
let combo = document.createElement("select");
|
||||
combo.className = "cb-widget";
|
||||
let default_packname_option = { value: '##manual', text: 'Packname: Manual' };
|
||||
|
||||
combo.appendChild($el('option', default_packname_option, []));
|
||||
for(let name in pack_map) {
|
||||
combo.appendChild($el('option', { value: name, text: 'Packname: '+ name }, []));
|
||||
}
|
||||
|
||||
let self = this;
|
||||
combo.onchange = function () {
|
||||
if(combo.selectedIndex == 0) {
|
||||
self.manual_packname.disabled = false;
|
||||
}
|
||||
else {
|
||||
self.manual_packname.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
return combo;
|
||||
}
|
||||
}
|
||||
|
||||
let orig_handleFile = app.handleFile;
|
||||
|
||||
async function handleFile(file) {
|
||||
if (file.name?.endsWith(".json") || file.name?.endsWith(".pack")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
let is_component = false;
|
||||
const jsonContent = JSON.parse(reader.result);
|
||||
for(let name in jsonContent) {
|
||||
let cand = jsonContent[name];
|
||||
is_component = cand.datetime && cand.version;
|
||||
break;
|
||||
}
|
||||
|
||||
if(is_component) {
|
||||
await handle_import_components(jsonContent);
|
||||
}
|
||||
else {
|
||||
orig_handleFile.call(app, file);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
orig_handleFile.call(app, file);
|
||||
}
|
||||
|
||||
app.handleFile = handleFile;
|
||||
|
||||
let current_component_policy = 'workflow';
|
||||
try {
|
||||
api.fetchApi('/v2/manager/policy/component')
|
||||
.then(response => response.text())
|
||||
.then(data => { current_component_policy = data; });
|
||||
}
|
||||
catch {}
|
||||
|
||||
function getChangedVersion(groupNodes) {
|
||||
if(!Object.keys(pack_map).length || !groupNodes)
|
||||
return null;
|
||||
|
||||
let res = {};
|
||||
for(let component_name in groupNodes) {
|
||||
let data = groupNodes[component_name];
|
||||
|
||||
if(rpack_map[component_name]) {
|
||||
let v = versionCompare(data.version, rpack_map[component_name].version);
|
||||
res[component_name] = v;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
const loadGraphData = app.loadGraphData;
|
||||
app.loadGraphData = async function () {
|
||||
if(arguments.length == 0)
|
||||
return await loadGraphData.apply(this, arguments);
|
||||
|
||||
let graphData = arguments[0];
|
||||
let groupNodes = graphData.extra?.groupNodes;
|
||||
let res = getChangedVersion(groupNodes);
|
||||
|
||||
if(res) {
|
||||
let target_components = null;
|
||||
switch(current_component_policy) {
|
||||
case 'higher':
|
||||
target_components = Object.keys(res).filter(key => res[key] == 1);
|
||||
break;
|
||||
|
||||
case 'mine':
|
||||
target_components = Object.keys(res);
|
||||
break;
|
||||
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if(target_components) {
|
||||
for(let i in target_components) {
|
||||
let component_name = target_components[i];
|
||||
let component = rpack_map[component_name];
|
||||
if(component && graphData.extra?.groupNodes) {
|
||||
graphData.extra.groupNodes[component_name] = component;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('Empty components: policy ignored');
|
||||
}
|
||||
|
||||
arguments[0] = graphData;
|
||||
return await loadGraphData.apply(this, arguments);
|
||||
};
|
||||
|
||||
export function set_component_policy(v) {
|
||||
current_component_policy = v;
|
||||
}
|
||||
|
||||
let graphToPrompt = app.graphToPrompt;
|
||||
app.graphToPrompt = async function () {
|
||||
let p = await graphToPrompt.call(app);
|
||||
try {
|
||||
let groupNodes = p.workflow.extra?.groupNodes;
|
||||
if(groupNodes) {
|
||||
p.workflow.extra = { ... p.workflow.extra};
|
||||
|
||||
// get used group nodes
|
||||
let used_group_nodes = new Set();
|
||||
for(let node of p.workflow.nodes) {
|
||||
if(node.type.startsWith(`workflow/`) || node.type.startsWith(`workflow${SEPARATOR}`)) {
|
||||
used_group_nodes.add(node.type.substring(9));
|
||||
}
|
||||
}
|
||||
|
||||
// remove unused group nodes
|
||||
let new_groupNodes = {};
|
||||
for (let key in p.workflow.extra.groupNodes) {
|
||||
if (used_group_nodes.has(key)) {
|
||||
new_groupNodes[key] = p.workflow.extra.groupNodes[key];
|
||||
}
|
||||
}
|
||||
p.workflow.extra.groupNodes = new_groupNodes;
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
console.log(`Failed to filtering group nodes: ${e}`);
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
.cn-manager {
|
||||
--grid-font: -apple-system, BlinkMacSystemFont, "Segue UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
--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%;
|
||||
width: 80vw;
|
||||
height: 75vh;
|
||||
min-height: 30em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
@@ -10,6 +11,7 @@
|
||||
font-family: arial, sans-serif;
|
||||
text-underline-offset: 3px;
|
||||
outline: none;
|
||||
margin: calc(var(--spacing)*2);
|
||||
}
|
||||
|
||||
.cn-manager .cn-flex-auto {
|
||||
@@ -17,17 +19,21 @@
|
||||
}
|
||||
|
||||
.cn-manager button {
|
||||
width: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
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:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
.cn-manager button:disabled,
|
||||
.cn-manager input:disabled,
|
||||
.cn-manager select:disabled {
|
||||
@@ -40,8 +46,13 @@
|
||||
|
||||
.cn-manager .cn-manager-restart {
|
||||
display: none;
|
||||
background-color: #500000;
|
||||
color: white;
|
||||
background-color: #500000 !important;
|
||||
border-color: #88181b !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.cn-manager .cn-manager-restart:hover {
|
||||
background-color: #88181b !important;
|
||||
}
|
||||
|
||||
.cn-manager .cn-manager-stop {
|
||||
@@ -79,7 +90,6 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.cn-manager-header label {
|
||||
@@ -91,16 +101,32 @@
|
||||
.cn-manager-filter {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
|
||||
cursor: pointer;
|
||||
padding: 0.5em 0.5em;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.cn-manager-filter:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
.cn-manager-keywords {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 5px 0 26px;
|
||||
background: var(--comfy-input-bg);
|
||||
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");
|
||||
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
|
||||
outline-color: transparent;
|
||||
}
|
||||
|
||||
.cn-manager-status {
|
||||
@@ -588,6 +614,10 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cn-install-buttons button {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.cn-selected-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
|
||||
|
||||
import {
|
||||
manager_instance, rebootAPI, install_via_git_url,
|
||||
@@ -18,32 +19,19 @@ loadCss("./custom-nodes-manager.css");
|
||||
const gridId = "node";
|
||||
|
||||
const pageHtml = `
|
||||
<div class="cn-manager-header">
|
||||
<label>Filter
|
||||
<select class="cn-manager-filter"></select>
|
||||
</label>
|
||||
<input class="cn-manager-keywords" type="search" placeholder="Search" />
|
||||
<div class="cn-manager-status"></div>
|
||||
<div class="cn-flex-auto"></div>
|
||||
<div class="cn-manager-channel"></div>
|
||||
</div>
|
||||
<div class="cn-manager-grid"></div>
|
||||
<div class="cn-manager-selection"></div>
|
||||
<div class="cn-manager-message"></div>
|
||||
<div class="cn-manager-footer">
|
||||
<button class="cn-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="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Back
|
||||
</button>
|
||||
<button class="cn-manager-restart">Restart</button>
|
||||
<button class="cn-manager-stop">Stop</button>
|
||||
<div class="cn-flex-auto"></div>
|
||||
<button class="cn-manager-used-in-workflow">Used In Workflow</button>
|
||||
<button class="cn-manager-check-update">Check Update</button>
|
||||
<button class="cn-manager-check-missing">Check Missing</button>
|
||||
<button class="cn-manager-install-url">Install via Git URL</button>
|
||||
<div class="cn-manager cn-manager-dark">
|
||||
<div class="cn-manager-grid"></div>
|
||||
<div class="cn-manager-selection"></div>
|
||||
<div class="cn-manager-message"></div>
|
||||
<div class="cn-manager-footer">
|
||||
<button class="cn-manager-restart p-button p-component">Restart</button>
|
||||
<button class="cn-manager-stop p-button p-component">Stop</button>
|
||||
<div class="cn-flex-auto"></div>
|
||||
<button class="cn-manager-used-in-workflow p-button p-component">Used In Workflow</button>
|
||||
<button class="cn-manager-check-update p-button p-component">Check Update</button>
|
||||
<button class="cn-manager-check-missing p-button p-component">Check Missing</button>
|
||||
<button class="cn-manager-install-url p-button p-component">Install via Git URL</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -89,11 +77,26 @@ export class CustomNodesManager {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.element = $el("div", {
|
||||
parent: document.body,
|
||||
className: "comfy-modal cn-manager"
|
||||
});
|
||||
this.element.innerHTML = pageHtml;
|
||||
const header = $el("div.cn-manager-header.px-2", {}, [
|
||||
// $el("label", {}, [
|
||||
// $el("span", { textContent: "Filter" }),
|
||||
// $el("select.cn-manager-filter")
|
||||
// ]),
|
||||
createSettingsCombo("Filter", $el("select.cn-manager-filter")),
|
||||
$el("input.cn-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
|
||||
$el("div.cn-manager-status"),
|
||||
$el("div.cn-flex-auto"),
|
||||
$el("div.cn-manager-channel")
|
||||
]);
|
||||
|
||||
const frame = buildGuiFrameCustomHeader(
|
||||
'cn-manager-dialog', // dialog id
|
||||
header, // custom header element
|
||||
pageHtml, // dialog content element
|
||||
this
|
||||
); // send this so we can attach close functions
|
||||
|
||||
this.element = frame;
|
||||
this.element.setAttribute("tabindex", 0);
|
||||
this.element.focus();
|
||||
|
||||
@@ -372,7 +375,7 @@ export class CustomNodesManager {
|
||||
|
||||
return list.map(id => {
|
||||
const bt = buttons[id];
|
||||
return `<button class="cn-btn-${id}" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
|
||||
return `<button class="cn-btn-${id} p-button p-component" group="${action}" mode="${bt.mode}">${bt.label}</button>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
@@ -655,7 +658,6 @@ export class CustomNodesManager {
|
||||
}
|
||||
|
||||
renderGrid() {
|
||||
|
||||
// update theme
|
||||
const globalStyle = window.getComputedStyle(document.body);
|
||||
this.colorVars = {
|
||||
@@ -714,6 +716,7 @@ export class CustomNodesManager {
|
||||
link.href = rowItem.reference;
|
||||
link.target = '_blank';
|
||||
link.innerHTML = `<b>${title}</b>`;
|
||||
link.title = rowItem.originalData.id;
|
||||
container.appendChild(link);
|
||||
|
||||
return container;
|
||||
@@ -1625,17 +1628,35 @@ export class CustomNodesManager {
|
||||
getNodesInWorkflow() {
|
||||
let usedGroupNodes = new Set();
|
||||
let allUsedNodes = {};
|
||||
const visitedGraphs = new Set();
|
||||
|
||||
for(let k in app.graph._nodes) {
|
||||
let node = app.graph._nodes[k];
|
||||
const visitGraph = (graph) => {
|
||||
if (!graph || visitedGraphs.has(graph)) return;
|
||||
visitedGraphs.add(graph);
|
||||
|
||||
if(node.type.startsWith('workflow>')) {
|
||||
usedGroupNodes.add(node.type.slice(9));
|
||||
continue;
|
||||
const nodes = graph._nodes || graph.nodes || [];
|
||||
for(let k in nodes) {
|
||||
let node = nodes[k];
|
||||
if (!node) continue;
|
||||
|
||||
// If it's a SubgraphNode, recurse into its graph and continue searching
|
||||
if (node.isSubgraphNode?.() && node.subgraph) {
|
||||
visitGraph(node.subgraph);
|
||||
}
|
||||
|
||||
if (!node.type) continue;
|
||||
|
||||
// Group nodes / components
|
||||
if(typeof node.type === 'string' && node.type.startsWith('workflow>')) {
|
||||
usedGroupNodes.add(node.type.slice(9));
|
||||
continue;
|
||||
}
|
||||
|
||||
allUsedNodes[node.type] = node;
|
||||
}
|
||||
};
|
||||
|
||||
allUsedNodes[node.type] = node;
|
||||
}
|
||||
visitGraph(app.graph);
|
||||
|
||||
for(let k of usedGroupNodes) {
|
||||
let subnodes = app.graph.extra.groupNodes[k]?.nodes;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
.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%;
|
||||
width: 80vw;
|
||||
height: 75vh;
|
||||
min-height: 30em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
color: var(--fg-color);
|
||||
font-family: arial, sans-serif;
|
||||
margin: calc(var(--spacing)*2);
|
||||
}
|
||||
|
||||
.cmm-manager .cmm-flex-auto {
|
||||
@@ -18,14 +20,15 @@
|
||||
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:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
.cmm-manager button:disabled,
|
||||
.cmm-manager input:disabled,
|
||||
.cmm-manager select:disabled {
|
||||
@@ -38,7 +41,7 @@
|
||||
|
||||
.cmm-manager .cmm-manager-refresh {
|
||||
display: none;
|
||||
background-color: #000080;
|
||||
background-color: #000080 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -53,7 +56,6 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.cmm-manager-header label {
|
||||
@@ -67,16 +69,34 @@
|
||||
.cmm-manager-filter {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
|
||||
cursor: pointer;
|
||||
padding: 0.5em 0.5em;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.cmm-manager-type:hover,
|
||||
.cmm-manager-base:hover,
|
||||
.cmm-manager-filter:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
.cmm-manager-keywords {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 5px 0 26px;
|
||||
background: var(--comfy-input-bg);
|
||||
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");
|
||||
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
|
||||
outline-color: transparent;
|
||||
}
|
||||
|
||||
.cmm-manager-status {
|
||||
@@ -148,6 +168,10 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cmm-btn-install {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.cmm-btn-download {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
@@ -9,39 +9,22 @@ import { api } from "../../scripts/api.js";
|
||||
|
||||
// https://cenfun.github.io/turbogrid/api.html
|
||||
import TG from "./turbogrid.esm.js";
|
||||
import { buildGuiFrameCustomHeader, createSettingsCombo } from "./comfyui-gui-builder.js";
|
||||
|
||||
loadCss("./model-manager.css");
|
||||
|
||||
const gridId = "model";
|
||||
|
||||
const pageHtml = `
|
||||
<div class="cmm-manager-header">
|
||||
<label>Filter
|
||||
<select class="cmm-manager-filter"></select>
|
||||
</label>
|
||||
<label>Type
|
||||
<select class="cmm-manager-type"></select>
|
||||
</label>
|
||||
<label>Base
|
||||
<select class="cmm-manager-base"></select>
|
||||
</label>
|
||||
<input class="cmm-manager-keywords" type="search" placeholder="Search" />
|
||||
<div class="cmm-manager-status"></div>
|
||||
<div class="cmm-flex-auto"></div>
|
||||
</div>
|
||||
<div class="cmm-manager-grid"></div>
|
||||
<div class="cmm-manager-selection"></div>
|
||||
<div class="cmm-manager-message"></div>
|
||||
<div class="cmm-manager-footer">
|
||||
<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-manager cmm-manager-dark">
|
||||
<div class="cmm-manager-grid"></div>
|
||||
<div class="cmm-manager-selection"></div>
|
||||
<div class="cmm-manager-message"></div>
|
||||
<div class="cmm-manager-footer">
|
||||
<button class="cmm-manager-refresh p-button p-component">Refresh</button>
|
||||
<button class="cmm-manager-stop p-button p-component">Stop</button>
|
||||
<div class="cmm-flex-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -64,11 +47,23 @@ export class ModelManager {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.element = $el("div", {
|
||||
parent: document.body,
|
||||
className: "comfy-modal cmm-manager"
|
||||
});
|
||||
this.element.innerHTML = pageHtml;
|
||||
const header = $el("div.cmm-manager-header", {}, [
|
||||
createSettingsCombo("Filter", $el("select.cmm-manager-filter")),
|
||||
createSettingsCombo("Type", $el("select.cmm-manager-type")),
|
||||
createSettingsCombo("Base", $el("select.cmm-manager-base")),
|
||||
$el("input.cmm-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
|
||||
$el("div.cmm-manager-status"),
|
||||
$el("div.cmm-flex-auto")
|
||||
]);
|
||||
|
||||
const frame = buildGuiFrameCustomHeader(
|
||||
'cmm-manager-dialog', // dialog id
|
||||
header, // custom header element
|
||||
pageHtml, // dialog content element
|
||||
this
|
||||
); // send this so we can attach close functions
|
||||
|
||||
this.element = frame;
|
||||
this.initFilter();
|
||||
this.bindEvents();
|
||||
this.initGrid();
|
||||
@@ -347,7 +342,7 @@ export class ModelManager {
|
||||
if (installed === "True") {
|
||||
return `<div class="cmm-icon-passed">${icons.passed}</div>`;
|
||||
}
|
||||
return `<button class="cmm-btn-install" mode="install">Install</button>`;
|
||||
return `<button class="cmm-btn-install p-button p-component" mode="install">Install</button>`;
|
||||
}
|
||||
}, {
|
||||
id: 'url',
|
||||
@@ -420,7 +415,7 @@ export class ModelManager {
|
||||
}
|
||||
|
||||
this.selectedModels = selectedList;
|
||||
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install" mode="install">Install</button>`);
|
||||
this.showSelection(`<span>Selected <b>${selectedList.length}</b> models <button class="cmm-btn-install p-button p-component" mode="install">Install</button>`);
|
||||
}
|
||||
|
||||
focusInstall(item) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
.snapshot-manager {
|
||||
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
z-index: 1099;
|
||||
width: 80vw;
|
||||
height: 75vh;
|
||||
min-height: 30em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
color: var(--fg-color);
|
||||
font-family: arial, sans-serif;
|
||||
text-underline-offset: 3px;
|
||||
outline: none;
|
||||
margin: calc(var(--spacing)*2);
|
||||
}
|
||||
|
||||
.snapshot-manager button {
|
||||
width: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-color: var(--border-color);
|
||||
margin: 0;
|
||||
min-width: 100px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.snapshot-manager .snapshot-restore-btn {
|
||||
background-color: #00158f !important;
|
||||
border-color: #2025b9 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.snapshot-manager .snapshot-remove-btn {
|
||||
background-color: #970000 !important;
|
||||
border-color: #be2127 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.snapshot-manager button:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
.snapshot-manager .data-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing)*2);
|
||||
padding: calc(var(--spacing)*2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.snapshot-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.snapshot-manager .cn-flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
import { manager_instance, rebootAPI, show_message } from "./common.js";
|
||||
import { manager_instance, rebootAPI, show_message, loadCss } from "./common.js";
|
||||
import { buildGuiFrame } from "./comfyui-gui-builder.js";
|
||||
|
||||
loadCss("./snapshot.css");
|
||||
|
||||
async function restore_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
@@ -27,7 +29,7 @@ async function restore_snapshot(target) {
|
||||
}
|
||||
finally {
|
||||
await SnapshotManager.instance.invalidateControl();
|
||||
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='cm-small-button'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
|
||||
SnapshotManager.instance.updateMessage("<BR>To apply the snapshot, please <button id='cm-reboot-button2' class='p-button p-component'>RESTART</button> ComfyUI. And refresh browser.", 'cm-reboot-button2');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,6 +90,8 @@ export class SnapshotManager extends ComfyDialog {
|
||||
message_box = null;
|
||||
data = null;
|
||||
|
||||
content = $el("div.snapshot-manager");
|
||||
|
||||
clear() {
|
||||
this.restore_buttons = [];
|
||||
this.message_box = null;
|
||||
@@ -96,9 +100,18 @@ export class SnapshotManager extends ComfyDialog {
|
||||
|
||||
constructor(app, manager_dialog) {
|
||||
super();
|
||||
this.manager_dialog = manager_dialog;
|
||||
// this.manager_dialog = manager_dialog;
|
||||
this.search_keyword = '';
|
||||
this.element = $el("div.comfy-modal", { parent: document.body }, []);
|
||||
|
||||
const frame = buildGuiFrame(
|
||||
'snapshot-manager-dialog', // dialog id
|
||||
'Snapshot Manager', // title
|
||||
'i.mdi.mdi-puzzle', // icon class
|
||||
this.content, // dialog content element
|
||||
this
|
||||
); // send this so we can attach close functions
|
||||
|
||||
this.element = frame;
|
||||
}
|
||||
|
||||
async remove_item() {
|
||||
@@ -109,7 +122,7 @@ export class SnapshotManager extends ComfyDialog {
|
||||
|
||||
createControls() {
|
||||
return [
|
||||
$el("button.cm-small-button", {
|
||||
$el("button.p-button.p-component", {
|
||||
type: "button",
|
||||
textContent: "Close",
|
||||
onclick: () => { this.close(); }
|
||||
@@ -132,8 +145,8 @@ export class SnapshotManager extends ComfyDialog {
|
||||
this.clear();
|
||||
this.data = (await getSnapshotList()).items;
|
||||
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
while (this.content.children.length) {
|
||||
this.content.removeChild(this.content.children[0]);
|
||||
}
|
||||
|
||||
await this.createGrid();
|
||||
@@ -204,20 +217,21 @@ export class SnapshotManager extends ComfyDialog {
|
||||
data2.innerHTML = ` ${data}`;
|
||||
var data_button = document.createElement('td');
|
||||
data_button.style.textAlign = "center";
|
||||
data_button.className = "data-btns";
|
||||
|
||||
var restoreBtn = document.createElement('button');
|
||||
restoreBtn.className = "snapshot-restore-btn p-button p-component";
|
||||
restoreBtn.innerHTML = 'Restore';
|
||||
restoreBtn.style.width = "100px";
|
||||
restoreBtn.style.backgroundColor = 'blue';
|
||||
|
||||
restoreBtn.addEventListener('click', function() {
|
||||
restore_snapshot(data);
|
||||
});
|
||||
|
||||
var removeBtn = document.createElement('button');
|
||||
removeBtn.className = "snapshot-remove-btn p-button p-component";
|
||||
removeBtn.innerHTML = 'Remove';
|
||||
removeBtn.style.width = "100px";
|
||||
removeBtn.style.backgroundColor = 'red';
|
||||
|
||||
removeBtn.addEventListener('click', function() {
|
||||
remove_snapshot(data);
|
||||
@@ -241,13 +255,14 @@ export class SnapshotManager extends ComfyDialog {
|
||||
let self = this;
|
||||
const panel = document.createElement('div');
|
||||
panel.style.width = "100%";
|
||||
panel.style.height = "100%";
|
||||
panel.appendChild(grid);
|
||||
|
||||
function handleResize() {
|
||||
const parentHeight = self.element.clientHeight;
|
||||
const gridHeight = parentHeight - 200;
|
||||
|
||||
grid.style.height = gridHeight + "px";
|
||||
// grid.style.height = gridHeight + "px";
|
||||
}
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
@@ -256,25 +271,17 @@ export class SnapshotManager extends ComfyDialog {
|
||||
grid.style.width = "100%";
|
||||
grid.style.height = "100%";
|
||||
grid.style.overflowY = "scroll";
|
||||
this.element.style.height = "85%";
|
||||
this.element.style.width = "80%";
|
||||
this.element.appendChild(panel);
|
||||
|
||||
this.content.appendChild(panel);
|
||||
|
||||
handleResize();
|
||||
}
|
||||
|
||||
async createBottomControls() {
|
||||
var close_button = document.createElement("button");
|
||||
close_button.className = "cm-small-button";
|
||||
close_button.innerHTML = "Close";
|
||||
close_button.onclick = () => { this.close(); }
|
||||
close_button.style.display = "inline-block";
|
||||
|
||||
var save_button = document.createElement("button");
|
||||
save_button.className = "cm-small-button";
|
||||
save_button.className = "p-button p-component";
|
||||
save_button.innerHTML = "Save snapshot";
|
||||
save_button.onclick = () => { save_current_snapshot(); }
|
||||
save_button.style.display = "inline-block";
|
||||
save_button.style.horizontalAlign = "right";
|
||||
save_button.style.width = "170px";
|
||||
|
||||
@@ -282,15 +289,19 @@ export class SnapshotManager extends ComfyDialog {
|
||||
this.message_box.style.height = '60px';
|
||||
this.message_box.style.verticalAlign = 'middle';
|
||||
|
||||
this.element.appendChild(this.message_box);
|
||||
this.element.appendChild(close_button);
|
||||
this.element.appendChild(save_button);
|
||||
const footer = $el("div.snapshot-footer");
|
||||
const spacer = $el("div.cn-flex-auto");
|
||||
footer.appendChild(spacer);
|
||||
footer.appendChild(save_button);
|
||||
|
||||
this.content.appendChild(this.message_box);
|
||||
this.content.appendChild(footer);
|
||||
}
|
||||
|
||||
async show() {
|
||||
try {
|
||||
this.invalidateControl();
|
||||
this.element.style.display = "block";
|
||||
this.element.style.display = "flex";
|
||||
this.element.style.zIndex = 1099;
|
||||
}
|
||||
catch(exception) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import platform
|
||||
from datetime import datetime
|
||||
|
||||
import git
|
||||
from comfyui_manager.common.timestamp_utils import get_timestamp_for_path, get_backup_branch_name
|
||||
from git.remote import RemoteProgress
|
||||
from urllib.parse import urlparse
|
||||
from tqdm.auto import tqdm
|
||||
@@ -41,12 +42,15 @@ from ..common.enums import NetworkMode, SecurityLevel, DBMode
|
||||
from ..common import context
|
||||
|
||||
|
||||
version_code = [4, 0]
|
||||
version_code = [4, 0, 4]
|
||||
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
|
||||
|
||||
|
||||
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main"
|
||||
DEFAULT_CHANNEL_LEGACY = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
|
||||
|
||||
# SSH git URL pattern (e.g., git@github.com:user/repo.git)
|
||||
SSH_URL_PATTERN = re.compile(r"^(.+@|ssh://).+:.+$")
|
||||
|
||||
default_custom_nodes_path = None
|
||||
|
||||
@@ -160,7 +164,7 @@ comfy_ui_revision = "Unknown"
|
||||
comfy_ui_commit_datetime = datetime(1900, 1, 1, 0, 0, 0)
|
||||
|
||||
channel_dict = None
|
||||
valid_channels = {'default', 'local'}
|
||||
valid_channels = {'default', 'local', DEFAULT_CHANNEL, DEFAULT_CHANNEL_LEGACY}
|
||||
channel_list = None
|
||||
|
||||
|
||||
@@ -304,18 +308,86 @@ class ManagedResult:
|
||||
return self
|
||||
|
||||
|
||||
class NormalizedKeyDict:
|
||||
def __init__(self):
|
||||
self._store = {}
|
||||
self._key_map = {}
|
||||
|
||||
def _normalize_key(self, key):
|
||||
if isinstance(key, str):
|
||||
return key.strip().lower()
|
||||
return key
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
norm_key = self._normalize_key(key)
|
||||
self._key_map[norm_key] = key
|
||||
self._store[key] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
norm_key = self._normalize_key(key)
|
||||
original_key = self._key_map[norm_key]
|
||||
return self._store[original_key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
norm_key = self._normalize_key(key)
|
||||
original_key = self._key_map.pop(norm_key)
|
||||
del self._store[original_key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return self._normalize_key(key) in self._key_map
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key] if key in self else default
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
if key in self:
|
||||
return self[key]
|
||||
self[key] = default
|
||||
return default
|
||||
|
||||
def pop(self, key, default=None):
|
||||
if key in self:
|
||||
val = self[key]
|
||||
del self[key]
|
||||
return val
|
||||
if default is not None:
|
||||
return default
|
||||
raise KeyError(key)
|
||||
|
||||
def keys(self):
|
||||
return self._store.keys()
|
||||
|
||||
def values(self):
|
||||
return self._store.values()
|
||||
|
||||
def items(self):
|
||||
return self._store.items()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._store)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._store)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self._store)
|
||||
|
||||
def to_dict(self):
|
||||
return dict(self._store)
|
||||
|
||||
|
||||
class UnifiedManager:
|
||||
def __init__(self):
|
||||
self.installed_node_packages: dict[str, InstalledNodePackage] = {}
|
||||
|
||||
self.cnr_inactive_nodes = {} # node_id -> node_version -> fullpath
|
||||
self.nightly_inactive_nodes = {} # node_id -> fullpath
|
||||
self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath
|
||||
self.active_nodes = {} # node_id -> node_version * fullpath
|
||||
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
|
||||
self.cnr_map = {} # node_id -> cnr info
|
||||
self.repo_cnr_map = {} # repo_url -> cnr info
|
||||
self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json
|
||||
self.cnr_inactive_nodes = NormalizedKeyDict() # node_id -> node_version -> fullpath
|
||||
self.nightly_inactive_nodes = NormalizedKeyDict() # node_id -> fullpath
|
||||
self.unknown_inactive_nodes = {} # node_id -> repo url * fullpath
|
||||
self.active_nodes = NormalizedKeyDict() # node_id -> node_version * fullpath
|
||||
self.unknown_active_nodes = {} # node_id -> repo url * fullpath
|
||||
self.cnr_map = NormalizedKeyDict() # node_id -> cnr info
|
||||
self.repo_cnr_map = {} # repo_url -> cnr info
|
||||
self.custom_node_map_cache = {} # (channel, mode) -> augmented custom node list json
|
||||
self.processed_install = set()
|
||||
|
||||
def get_module_name(self, x):
|
||||
@@ -721,7 +793,7 @@ class UnifiedManager:
|
||||
channel = normalize_channel(channel)
|
||||
nodes = await self.load_nightly(channel, mode)
|
||||
|
||||
res = {}
|
||||
res = NormalizedKeyDict()
|
||||
added_cnr = set()
|
||||
for v in nodes.values():
|
||||
v = v[0]
|
||||
@@ -1322,6 +1394,7 @@ class UnifiedManager:
|
||||
return ManagedResult('skip')
|
||||
elif self.is_disabled(node_id):
|
||||
return self.unified_enable(node_id)
|
||||
|
||||
else:
|
||||
version_spec = self.resolve_unspecified_version(node_id)
|
||||
|
||||
@@ -1506,9 +1579,6 @@ class ManagerFuncs:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_current_preview_method(self):
|
||||
return "none"
|
||||
|
||||
def run_script(self, cmd, cwd='.'):
|
||||
if len(cmd) > 0 and cmd[0].startswith("#"):
|
||||
print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`")
|
||||
@@ -1526,14 +1596,12 @@ def write_config():
|
||||
config = configparser.ConfigParser(strict=False)
|
||||
|
||||
config['default'] = {
|
||||
'preview_method': manager_funcs.get_current_preview_method(),
|
||||
'git_exe': get_config()['git_exe'],
|
||||
'use_uv': get_config()['use_uv'],
|
||||
'channel_url': get_config()['channel_url'],
|
||||
'share_option': get_config()['share_option'],
|
||||
'bypass_ssl': get_config()['bypass_ssl'],
|
||||
"file_logging": get_config()['file_logging'],
|
||||
'component_policy': get_config()['component_policy'],
|
||||
'update_policy': get_config()['update_policy'],
|
||||
'windows_selector_event_loop_policy': get_config()['windows_selector_event_loop_policy'],
|
||||
'model_download_by_agent': get_config()['model_download_by_agent'],
|
||||
@@ -1557,22 +1625,22 @@ def read_config():
|
||||
config = configparser.ConfigParser(strict=False)
|
||||
config.read(context.manager_config_path)
|
||||
default_conf = config['default']
|
||||
manager_util.use_uv = default_conf['use_uv'].lower() == 'true' if 'use_uv' in default_conf else False
|
||||
|
||||
def get_bool(key, default_value):
|
||||
return default_conf[key].lower() == 'true' if key in default_conf else False
|
||||
|
||||
manager_util.use_uv = default_conf['use_uv'].lower() == 'true' if 'use_uv' in default_conf else False
|
||||
manager_util.bypass_ssl = get_bool('bypass_ssl', False)
|
||||
|
||||
return {
|
||||
'http_channel_enabled': get_bool('http_channel_enabled', False),
|
||||
'preview_method': default_conf.get('preview_method', manager_funcs.get_current_preview_method()).lower(),
|
||||
'git_exe': default_conf.get('git_exe', ''),
|
||||
'use_uv': get_bool('use_uv', False),
|
||||
'use_uv': get_bool('use_uv', True),
|
||||
'channel_url': default_conf.get('channel_url', DEFAULT_CHANNEL),
|
||||
'default_cache_as_channel_url': get_bool('default_cache_as_channel_url', False),
|
||||
'share_option': default_conf.get('share_option', 'all').lower(),
|
||||
'bypass_ssl': get_bool('bypass_ssl', False),
|
||||
'file_logging': get_bool('file_logging', True),
|
||||
'component_policy': default_conf.get('component_policy', 'workflow').lower(),
|
||||
'update_policy': default_conf.get('update_policy', 'stable-comfyui').lower(),
|
||||
'windows_selector_event_loop_policy': get_bool('windows_selector_event_loop_policy', False),
|
||||
'model_download_by_agent': get_bool('model_download_by_agent', False),
|
||||
@@ -1585,17 +1653,17 @@ def read_config():
|
||||
|
||||
except Exception:
|
||||
manager_util.use_uv = False
|
||||
manager_util.bypass_ssl = False
|
||||
|
||||
return {
|
||||
'http_channel_enabled': False,
|
||||
'preview_method': manager_funcs.get_current_preview_method(),
|
||||
'git_exe': '',
|
||||
'use_uv': False,
|
||||
'use_uv': True,
|
||||
'channel_url': DEFAULT_CHANNEL,
|
||||
'default_cache_as_channel_url': False,
|
||||
'share_option': 'all',
|
||||
'bypass_ssl': False,
|
||||
'bypass_ssl': manager_util.bypass_ssl,
|
||||
'file_logging': True,
|
||||
'component_policy': 'workflow',
|
||||
'update_policy': 'stable-comfyui',
|
||||
'windows_selector_event_loop_policy': False,
|
||||
'model_download_by_agent': False,
|
||||
@@ -1839,6 +1907,27 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
|
||||
return True
|
||||
|
||||
|
||||
def install_manager_requirements(repo_path):
|
||||
"""
|
||||
Install packages from manager_requirements.txt if it exists.
|
||||
This is specifically for ComfyUI's manager_requirements.txt.
|
||||
"""
|
||||
manager_requirements_path = os.path.join(repo_path, "manager_requirements.txt")
|
||||
if not os.path.exists(manager_requirements_path):
|
||||
return
|
||||
|
||||
logging.info("[ComfyUI-Manager] Installing manager_requirements.txt")
|
||||
with open(manager_requirements_path, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
if '#' in line:
|
||||
line = line.split('#')[0].strip()
|
||||
if line:
|
||||
install_cmd = manager_util.make_pip_cmd(["install", line])
|
||||
subprocess.run(install_cmd)
|
||||
|
||||
|
||||
def git_repo_update_check_with(path, do_fetch=False, do_update=False, no_deps=False):
|
||||
"""
|
||||
|
||||
@@ -1917,7 +2006,15 @@ def git_repo_update_check_with(path, do_fetch=False, do_update=False, no_deps=Fa
|
||||
return False, True
|
||||
|
||||
try:
|
||||
remote.pull()
|
||||
try:
|
||||
repo.git.pull('--ff-only')
|
||||
except git.GitCommandError:
|
||||
backup_name = get_backup_branch_name(repo)
|
||||
repo.create_head(backup_name)
|
||||
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
|
||||
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
|
||||
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
|
||||
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
new_commit_hash = repo.head.commit.hexsha
|
||||
|
||||
@@ -1974,16 +2071,15 @@ class GitProgress(RemoteProgress):
|
||||
|
||||
|
||||
def is_valid_url(url):
|
||||
try:
|
||||
# Check for HTTP/HTTPS URL format
|
||||
result = urlparse(url)
|
||||
if all([result.scheme, result.netloc]):
|
||||
return True
|
||||
finally:
|
||||
# Check for SSH git URL format
|
||||
pattern = re.compile(r"^(.+@|ssh://).+:.+$")
|
||||
if pattern.match(url):
|
||||
return True
|
||||
# Check for HTTP/HTTPS URL format
|
||||
result = urlparse(url)
|
||||
if result.scheme and result.netloc:
|
||||
return True
|
||||
|
||||
# Check for SSH git URL format
|
||||
if SSH_URL_PATTERN.match(url):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -2072,9 +2168,17 @@ def git_pull(path):
|
||||
|
||||
current_branch = repo.active_branch
|
||||
remote_name = current_branch.tracking_branch().remote_name
|
||||
remote = repo.remote(name=remote_name)
|
||||
branch_name = current_branch.name
|
||||
|
||||
try:
|
||||
repo.git.pull('--ff-only')
|
||||
except git.GitCommandError:
|
||||
backup_name = get_backup_branch_name(repo)
|
||||
repo.create_head(backup_name)
|
||||
logging.info(f"[ComfyUI-Manager] Cannot fast-forward. Backup created: {backup_name}")
|
||||
repo.git.reset('--hard', f'{remote_name}/{branch_name}')
|
||||
logging.info(f"[ComfyUI-Manager] Reset to {remote_name}/{branch_name}")
|
||||
|
||||
remote.pull()
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
|
||||
repo.close()
|
||||
@@ -2342,22 +2446,23 @@ def update_to_stable_comfyui(repo_path):
|
||||
logging.error('\t'+branch.name)
|
||||
return "fail", None
|
||||
|
||||
versions, current_tag, _ = get_comfyui_versions(repo)
|
||||
|
||||
if len(versions) == 0 or (len(versions) == 1 and versions[0] == 'nightly'):
|
||||
versions, current_tag, latest_tag = get_comfyui_versions(repo)
|
||||
|
||||
if latest_tag is None:
|
||||
logging.info("[ComfyUI-Manager] Unable to update to the stable ComfyUI version.")
|
||||
return "fail", None
|
||||
|
||||
if versions[0] == 'nightly':
|
||||
latest_tag = versions[1]
|
||||
else:
|
||||
latest_tag = versions[0]
|
||||
|
||||
if current_tag == latest_tag:
|
||||
tag_ref = next((t for t in repo.tags if t.name == latest_tag), None)
|
||||
if tag_ref is None:
|
||||
logging.info(f"[ComfyUI-Manager] Unable to locate tag '{latest_tag}' in repository.")
|
||||
return "fail", None
|
||||
|
||||
if repo.head.commit == tag_ref.commit:
|
||||
return "skip", None
|
||||
else:
|
||||
logging.info(f"[ComfyUI-Manager] Updating ComfyUI: {current_tag} -> {latest_tag}")
|
||||
repo.git.checkout(latest_tag)
|
||||
repo.git.checkout(tag_ref.name)
|
||||
execute_install_script("ComfyUI", repo_path, instant_execution=False, no_deps=False)
|
||||
return 'updated', latest_tag
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
@@ -2489,9 +2594,13 @@ def check_state_of_git_node_pack_single(item, do_fetch=False, do_update_check=Tr
|
||||
|
||||
|
||||
def get_installed_pip_packages():
|
||||
# extract pip package infos
|
||||
cmd = manager_util.make_pip_cmd(['freeze'])
|
||||
pips = subprocess.check_output(cmd, text=True).split('\n')
|
||||
try:
|
||||
# extract pip package infos
|
||||
cmd = manager_util.make_pip_cmd(['freeze'])
|
||||
pips = subprocess.check_output(cmd, text=True).split('\n')
|
||||
except Exception as e:
|
||||
logging.warning("[ComfyUI-Manager] Could not enumerate pip packages for snapshot: %s", e)
|
||||
return {}
|
||||
|
||||
res = {}
|
||||
for x in pips:
|
||||
@@ -2583,9 +2692,7 @@ async def get_current_snapshot(custom_nodes_only = False):
|
||||
|
||||
async def save_snapshot_with_postfix(postfix, path=None, custom_nodes_only = False):
|
||||
if path is None:
|
||||
now = datetime.now()
|
||||
|
||||
date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S")
|
||||
date_time_format = get_timestamp_for_path()
|
||||
file_name = f"{date_time_format}_{postfix}"
|
||||
|
||||
path = os.path.join(context.manager_snapshot_path, f"{file_name}.json")
|
||||
@@ -2776,7 +2883,7 @@ async def get_unified_total_nodes(channel, mode, regsitry_cache_mode='cache'):
|
||||
|
||||
if cnr_id is not None:
|
||||
# cnr or nightly version
|
||||
cnr_ids.remove(cnr_id)
|
||||
cnr_ids.discard(cnr_id)
|
||||
updatable = False
|
||||
cnr = unified_manager.cnr_map[cnr_id]
|
||||
|
||||
@@ -2940,6 +3047,11 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
|
||||
info = yaml.load(snapshot_file, Loader=yaml.SafeLoader)
|
||||
info = info['custom_nodes']
|
||||
|
||||
if 'pips' in info and info['pips']:
|
||||
pips = info['pips']
|
||||
else:
|
||||
pips = {}
|
||||
|
||||
# for cnr restore
|
||||
cnr_info = info.get('cnr_custom_nodes')
|
||||
if cnr_info is not None:
|
||||
@@ -3146,6 +3258,8 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
|
||||
unified_manager.repo_install(repo_url, to_path, instant_execution=True, no_deps=False, return_postinstall=False)
|
||||
cloned_repos.append(repo_name)
|
||||
|
||||
manager_util.restore_pip_snapshot(pips, git_helper_extras)
|
||||
|
||||
# print summary
|
||||
for x in cloned_repos:
|
||||
print(f"[ INSTALLED ] {x}")
|
||||
@@ -3169,36 +3283,85 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
|
||||
|
||||
|
||||
def get_comfyui_versions(repo=None):
|
||||
if repo is None:
|
||||
repo = git.Repo(context.comfy_path)
|
||||
repo = repo or git.Repo(context.comfy_path)
|
||||
|
||||
remote_name = None
|
||||
try:
|
||||
remote = get_remote_name(repo)
|
||||
repo.remotes[remote].fetch()
|
||||
remote_name = get_remote_name(repo)
|
||||
repo.remotes[remote_name].fetch()
|
||||
except Exception:
|
||||
logging.error("[ComfyUI-Manager] Failed to fetch ComfyUI")
|
||||
|
||||
versions = [x.name for x in repo.tags if x.name.startswith('v')]
|
||||
def parse_semver(tag_name):
|
||||
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', tag_name)
|
||||
return tuple(int(x) for x in match.groups()) if match else None
|
||||
|
||||
# nearest tag
|
||||
versions = sorted(versions, key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
|
||||
versions = versions[:4]
|
||||
def normalize_describe(tag_name):
|
||||
if not tag_name:
|
||||
return None
|
||||
base = tag_name.split('-', 1)[0]
|
||||
return base if parse_semver(base) else None
|
||||
|
||||
current_tag = repo.git.describe('--tags')
|
||||
# Collect semver tags and sort descending (highest first)
|
||||
semver_tags = []
|
||||
for tag in repo.tags:
|
||||
semver = parse_semver(tag.name)
|
||||
if semver:
|
||||
semver_tags.append((semver, tag.name))
|
||||
semver_tags.sort(key=lambda x: x[0], reverse=True)
|
||||
semver_tags = [name for _, name in semver_tags]
|
||||
|
||||
if current_tag not in versions:
|
||||
versions = sorted(versions + [current_tag], key=lambda v: repo.git.log('-1', '--format=%ct', v), reverse=True)
|
||||
versions = versions[:4]
|
||||
latest_tag = semver_tags[0] if semver_tags else None
|
||||
|
||||
main_branch = repo.heads.master
|
||||
latest_commit = main_branch.commit
|
||||
latest_tag = repo.git.describe('--tags', latest_commit.hexsha)
|
||||
try:
|
||||
described = repo.git.describe('--tags')
|
||||
except Exception:
|
||||
described = ''
|
||||
|
||||
if latest_tag != versions[0]:
|
||||
versions.insert(0, 'nightly')
|
||||
else:
|
||||
versions[0] = 'nightly'
|
||||
try:
|
||||
exact_tag = repo.git.describe('--tags', '--exact-match')
|
||||
except Exception:
|
||||
exact_tag = ''
|
||||
|
||||
head_is_default = False
|
||||
if remote_name:
|
||||
try:
|
||||
default_head_ref = repo.refs[f'{remote_name}/HEAD']
|
||||
default_commit = default_head_ref.reference.commit
|
||||
head_is_default = repo.head.commit == default_commit
|
||||
except Exception:
|
||||
# Fallback: compare directly with master branch
|
||||
try:
|
||||
if 'master' in [h.name for h in repo.heads]:
|
||||
head_is_default = repo.head.commit == repo.heads.master.commit
|
||||
except Exception:
|
||||
head_is_default = False
|
||||
|
||||
nearest_semver = normalize_describe(described)
|
||||
exact_semver = exact_tag if parse_semver(exact_tag) else None
|
||||
|
||||
if head_is_default and not exact_tag:
|
||||
current_tag = 'nightly'
|
||||
else:
|
||||
current_tag = exact_tag or described or 'nightly'
|
||||
|
||||
# Prepare semver list for display: top 4 plus the current/nearest semver if missing
|
||||
display_semver_tags = semver_tags[:4]
|
||||
if exact_semver and exact_semver not in display_semver_tags:
|
||||
display_semver_tags.append(exact_semver)
|
||||
elif nearest_semver and nearest_semver not in display_semver_tags:
|
||||
display_semver_tags.append(nearest_semver)
|
||||
|
||||
versions = ['nightly']
|
||||
|
||||
if current_tag and not exact_semver and current_tag not in versions and current_tag not in display_semver_tags:
|
||||
versions.append(current_tag)
|
||||
|
||||
for tag in display_semver_tags:
|
||||
if tag not in versions:
|
||||
versions.append(tag)
|
||||
|
||||
versions = versions[:6]
|
||||
|
||||
return versions, current_tag, latest_tag
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from ..common import manager_util
|
||||
from ..common import cm_global
|
||||
from ..common import manager_downloader
|
||||
from ..common import context
|
||||
from ..common import manager_security
|
||||
|
||||
|
||||
logging.info(f"### Loading: ComfyUI-Manager ({core.version_str})")
|
||||
@@ -36,7 +37,8 @@ logging.info("[ComfyUI-Manager] network_mode: " + network_mode_description)
|
||||
comfy_ui_hash = "-"
|
||||
comfyui_tag = None
|
||||
|
||||
SECURITY_MESSAGE_MIDDLE_OR_BELOW = "ERROR: To use this action, a security_level of `middle or below` is required. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_MIDDLE = "ERROR: To use this action, a security_level of `normal or below` is required. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_MIDDLE_P = "ERROR: To use this action, security_level must be `normal or below`, and network_mode must be set to `personal_cloud`. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_NORMAL_MINUS = "ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/Comfy-Org/ComfyUI-Manager#security-policy"
|
||||
SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower."
|
||||
@@ -59,7 +61,6 @@ def handle_stream(stream, prefix):
|
||||
|
||||
|
||||
from comfy.cli_args import args
|
||||
import latent_preview
|
||||
|
||||
def is_loopback(address):
|
||||
import ipaddress
|
||||
@@ -93,13 +94,27 @@ model_dir_name_map = {
|
||||
|
||||
|
||||
def is_allowed_security_level(level):
|
||||
is_personal_cloud = core.get_config()['network_mode'].lower() == 'personal_cloud'
|
||||
|
||||
if level == 'block':
|
||||
return False
|
||||
elif level == 'high+':
|
||||
if is_local_mode:
|
||||
return core.get_config()['security_level'] in ['weak', 'normal-']
|
||||
elif is_personal_cloud:
|
||||
return core.get_config()['security_level'] == 'weak'
|
||||
else:
|
||||
return False
|
||||
elif level == 'high':
|
||||
if is_local_mode:
|
||||
return core.get_config()['security_level'] in ['weak', 'normal-']
|
||||
else:
|
||||
return core.get_config()['security_level'] == 'weak'
|
||||
elif level == 'middle+':
|
||||
if is_local_mode or is_personal_cloud:
|
||||
return core.get_config()['security_level'] in ['weak', 'normal', 'normal-']
|
||||
else:
|
||||
return False
|
||||
elif level == 'middle':
|
||||
return core.get_config()['security_level'] in ['weak', 'normal', 'normal-']
|
||||
else:
|
||||
@@ -116,7 +131,7 @@ async def get_risky_level(files, pip_packages):
|
||||
|
||||
for x in files:
|
||||
if x not in all_urls:
|
||||
return "high"
|
||||
return "high+"
|
||||
|
||||
all_pip_packages = set()
|
||||
for x in json_data1['custom_nodes'] + json_data2['custom_nodes']:
|
||||
@@ -126,20 +141,10 @@ async def get_risky_level(files, pip_packages):
|
||||
if p not in all_pip_packages:
|
||||
return "block"
|
||||
|
||||
return "middle"
|
||||
return "middle+"
|
||||
|
||||
|
||||
class ManagerFuncsInComfyUI(core.ManagerFuncs):
|
||||
def get_current_preview_method(self):
|
||||
if args.preview_method == latent_preview.LatentPreviewMethod.Auto:
|
||||
return "auto"
|
||||
elif args.preview_method == latent_preview.LatentPreviewMethod.Latent2RGB:
|
||||
return "latent2rgb"
|
||||
elif args.preview_method == latent_preview.LatentPreviewMethod.TAESD:
|
||||
return "taesd"
|
||||
else:
|
||||
return "none"
|
||||
|
||||
def run_script(self, cmd, cwd='.'):
|
||||
if len(cmd) > 0 and cmd[0].startswith("#"):
|
||||
logging.error(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`")
|
||||
@@ -172,25 +177,6 @@ local_db_custom_node_list = os.path.join(manager_util.comfyui_manager_path, "cus
|
||||
local_db_extension_node_mappings = os.path.join(manager_util.comfyui_manager_path, "extension-node-map.json")
|
||||
|
||||
|
||||
def set_preview_method(method):
|
||||
if method == 'auto':
|
||||
args.preview_method = latent_preview.LatentPreviewMethod.Auto
|
||||
elif method == 'latent2rgb':
|
||||
args.preview_method = latent_preview.LatentPreviewMethod.Latent2RGB
|
||||
elif method == 'taesd':
|
||||
args.preview_method = latent_preview.LatentPreviewMethod.TAESD
|
||||
else:
|
||||
args.preview_method = latent_preview.LatentPreviewMethod.NoPreviews
|
||||
|
||||
core.get_config()['preview_method'] = method
|
||||
|
||||
|
||||
set_preview_method(core.get_config()['preview_method'])
|
||||
|
||||
|
||||
def set_component_policy(mode):
|
||||
core.get_config()['component_policy'] = mode
|
||||
|
||||
def set_update_policy(mode):
|
||||
core.get_config()['update_policy'] = mode
|
||||
|
||||
@@ -545,6 +531,8 @@ async def task_worker():
|
||||
logging.error("ComfyUI update failed")
|
||||
return "fail"
|
||||
elif res == "updated":
|
||||
core.install_manager_requirements(repo_path)
|
||||
|
||||
if is_stable:
|
||||
logging.info("ComfyUI is updated to latest stable version.")
|
||||
return "success-stable-"+latest_tag
|
||||
@@ -650,7 +638,7 @@ async def task_worker():
|
||||
return 'success'
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[ComfyUI-Manager] ERROR: {e}", file=sys.stderr)
|
||||
logging.error(f"[ComfyUI-Manager] ERROR: {e}")
|
||||
|
||||
return f"Model installation error: {model_url}"
|
||||
|
||||
@@ -758,29 +746,29 @@ async def queue_batch(request):
|
||||
for x in v:
|
||||
res = await _uninstall_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
failed.add(x['id'])
|
||||
else:
|
||||
res = await _install_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
failed.add(x['id'])
|
||||
|
||||
elif k == 'install':
|
||||
for x in v:
|
||||
res = await _install_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
failed.add(x['id'])
|
||||
|
||||
elif k == 'uninstall':
|
||||
for x in v:
|
||||
res = await _uninstall_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
failed.add(x['id'])
|
||||
|
||||
elif k == 'update':
|
||||
for x in v:
|
||||
res = await _update_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
failed.add(x['id'])
|
||||
|
||||
elif k == 'update_comfyui':
|
||||
await update_comfyui(None)
|
||||
@@ -793,13 +781,13 @@ async def queue_batch(request):
|
||||
for x in v:
|
||||
res = await _install_model(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
failed.add(x['id'])
|
||||
|
||||
elif k == 'fix':
|
||||
for x in v:
|
||||
res = await _fix_custom_node(x)
|
||||
if res.status != 200:
|
||||
failed.add(x[0])
|
||||
failed.add(x['id'])
|
||||
|
||||
with task_worker_lock:
|
||||
finalize_temp_queue_batch(json_data, failed)
|
||||
@@ -910,8 +898,8 @@ async def update_all(request):
|
||||
|
||||
|
||||
async def _update_all(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
if not is_allowed_security_level('middle+'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_P)
|
||||
return web.Response(status=403)
|
||||
|
||||
with task_worker_lock:
|
||||
@@ -1056,14 +1044,17 @@ async def fetch_customnode_list(request):
|
||||
if channel != 'local':
|
||||
found = 'custom'
|
||||
|
||||
for name, url in core.get_channel_dict().items():
|
||||
if url == channel:
|
||||
found = name
|
||||
break
|
||||
if channel == core.DEFAULT_CHANNEL or channel == core.DEFAULT_CHANNEL_LEGACY:
|
||||
channel = 'default'
|
||||
else:
|
||||
for name, url in core.get_channel_dict().items():
|
||||
if url == channel:
|
||||
found = name
|
||||
break
|
||||
|
||||
channel = found
|
||||
channel = found
|
||||
|
||||
result = dict(channel=channel, node_packs=node_packs)
|
||||
result = dict(channel=channel, node_packs=node_packs.to_dict())
|
||||
|
||||
return web.json_response(result, content_type='application/json')
|
||||
|
||||
@@ -1162,7 +1153,7 @@ async def get_snapshot_list(request):
|
||||
@routes.get("/v2/snapshot/remove")
|
||||
async def remove_snapshot(request):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
|
||||
try:
|
||||
@@ -1179,8 +1170,8 @@ async def remove_snapshot(request):
|
||||
|
||||
@routes.get("/v2/snapshot/restore")
|
||||
async def restore_snapshot(request):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
if not is_allowed_security_level('middle+'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_P)
|
||||
return web.Response(status=403)
|
||||
|
||||
try:
|
||||
@@ -1292,6 +1283,65 @@ async def import_fail_info(request):
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.post("/v2/customnode/import_fail_info_bulk")
|
||||
async def import_fail_info_bulk(request):
|
||||
try:
|
||||
json_data = await request.json()
|
||||
|
||||
# Basic validation - ensure we have either cnr_ids or urls
|
||||
if not isinstance(json_data, dict):
|
||||
return web.Response(status=400, text="Request body must be a JSON object")
|
||||
|
||||
if "cnr_ids" not in json_data and "urls" not in json_data:
|
||||
return web.Response(
|
||||
status=400, text="Either 'cnr_ids' or 'urls' field is required"
|
||||
)
|
||||
|
||||
await core.unified_manager.reload('cache')
|
||||
await core.unified_manager.get_custom_nodes('default', 'cache')
|
||||
|
||||
results = {}
|
||||
|
||||
if "cnr_ids" in json_data:
|
||||
if not isinstance(json_data["cnr_ids"], list):
|
||||
return web.Response(status=400, text="'cnr_ids' must be an array")
|
||||
for cnr_id in json_data["cnr_ids"]:
|
||||
if not isinstance(cnr_id, str):
|
||||
results[cnr_id] = {"error": "cnr_id must be a string"}
|
||||
continue
|
||||
module_name = core.unified_manager.get_module_name(cnr_id)
|
||||
if module_name is not None:
|
||||
info = cm_global.error_dict.get(module_name)
|
||||
if info is not None:
|
||||
results[cnr_id] = info
|
||||
else:
|
||||
results[cnr_id] = None
|
||||
else:
|
||||
results[cnr_id] = None
|
||||
|
||||
if "urls" in json_data:
|
||||
if not isinstance(json_data["urls"], list):
|
||||
return web.Response(status=400, text="'urls' must be an array")
|
||||
for url in json_data["urls"]:
|
||||
if not isinstance(url, str):
|
||||
results[url] = {"error": "url must be a string"}
|
||||
continue
|
||||
module_name = core.unified_manager.get_module_name(url)
|
||||
if module_name is not None:
|
||||
info = cm_global.error_dict.get(module_name)
|
||||
if info is not None:
|
||||
results[url] = info
|
||||
else:
|
||||
results[url] = None
|
||||
else:
|
||||
results[url] = None
|
||||
|
||||
return web.json_response(results)
|
||||
except Exception as e:
|
||||
logging.error(f"[ComfyUI-Manager] Error processing bulk import fail info: {e}")
|
||||
return web.Response(status=500, text="Internal server error")
|
||||
|
||||
|
||||
@routes.post("/v2/manager/queue/reinstall")
|
||||
async def reinstall_custom_node(request):
|
||||
await uninstall_custom_node(request)
|
||||
@@ -1356,8 +1406,8 @@ async def install_custom_node(request):
|
||||
|
||||
|
||||
async def _install_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
if not is_allowed_security_level('middle+'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_P)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
# non-nightly cnr is safe
|
||||
@@ -1462,7 +1512,7 @@ async def _fix_custom_node(json_data):
|
||||
|
||||
@routes.post("/v2/customnode/install/git_url")
|
||||
async def install_custom_node_git_url(request):
|
||||
if not is_allowed_security_level('high'):
|
||||
if not is_allowed_security_level('high+'):
|
||||
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
|
||||
return web.Response(status=403)
|
||||
|
||||
@@ -1482,7 +1532,7 @@ async def install_custom_node_git_url(request):
|
||||
|
||||
@routes.post("/v2/customnode/install/pip")
|
||||
async def install_custom_node_pip(request):
|
||||
if not is_allowed_security_level('high'):
|
||||
if not is_allowed_security_level('high+'):
|
||||
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
|
||||
return web.Response(status=403)
|
||||
|
||||
@@ -1500,7 +1550,7 @@ async def uninstall_custom_node(request):
|
||||
|
||||
async def _uninstall_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
node_id = json_data.get('id')
|
||||
@@ -1526,7 +1576,7 @@ async def update_custom_node(request):
|
||||
|
||||
async def _update_custom_node(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
node_id = json_data.get('id')
|
||||
@@ -1617,8 +1667,8 @@ async def install_model(request):
|
||||
|
||||
|
||||
async def _install_model(json_data):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
if not is_allowed_security_level('middle+'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_P)
|
||||
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
|
||||
|
||||
# validate request
|
||||
@@ -1626,7 +1676,7 @@ async def _install_model(json_data):
|
||||
logging.error(f"[ComfyUI-Manager] Invalid model install request is detected: {json_data}")
|
||||
return web.Response(status=400, text="Invalid model install request is detected")
|
||||
|
||||
if not json_data['filename'].endswith('.safetensors') and not is_allowed_security_level('high'):
|
||||
if not json_data['filename'].endswith('.safetensors') and not is_allowed_security_level('high+'):
|
||||
models_json = await core.get_data_by_mode('cache', 'model-list.json', 'default')
|
||||
|
||||
is_belongs_to_whitelist = False
|
||||
@@ -1645,17 +1695,6 @@ async def _install_model(json_data):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/preview_method")
|
||||
async def preview_method(request):
|
||||
if "value" in request.rel_url.query:
|
||||
set_preview_method(request.rel_url.query['value'])
|
||||
core.write_config()
|
||||
else:
|
||||
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/db_mode")
|
||||
async def db_mode(request):
|
||||
if "value" in request.rel_url.query:
|
||||
@@ -1667,18 +1706,6 @@ async def db_mode(request):
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
|
||||
@routes.get("/v2/manager/policy/component")
|
||||
async def component_policy(request):
|
||||
if "value" in request.rel_url.query:
|
||||
set_component_policy(request.rel_url.query['value'])
|
||||
core.write_config()
|
||||
else:
|
||||
return web.Response(text=core.get_config()['component_policy'], status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@routes.get("/v2/manager/policy/update")
|
||||
async def update_policy(request):
|
||||
if "value" in request.rel_url.query:
|
||||
@@ -1783,7 +1810,7 @@ async def get_notice_legacy(request):
|
||||
@routes.get("/v2/manager/reboot")
|
||||
def restart(self):
|
||||
if not is_allowed_security_level('middle'):
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
|
||||
logging.error(SECURITY_MESSAGE_MIDDLE)
|
||||
return web.Response(status=403)
|
||||
|
||||
try:
|
||||
@@ -1817,61 +1844,6 @@ def restart(self):
|
||||
return os.execv(sys.executable, cmds)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/component/save")
|
||||
async def save_component(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
name = data['name']
|
||||
workflow = data['workflow']
|
||||
|
||||
if not os.path.exists(context.manager_components_path):
|
||||
os.mkdir(context.manager_components_path)
|
||||
|
||||
if 'packname' in workflow and workflow['packname'] != '':
|
||||
sanitized_name = manager_util.sanitize_filename(workflow['packname']) + '.pack'
|
||||
else:
|
||||
sanitized_name = manager_util.sanitize_filename(name) + '.json'
|
||||
|
||||
filepath = os.path.join(context.manager_components_path, sanitized_name)
|
||||
components = {}
|
||||
if os.path.exists(filepath):
|
||||
with open(filepath) as f:
|
||||
components = json.load(f)
|
||||
|
||||
components[name] = workflow
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(components, f, indent=4, sort_keys=True)
|
||||
return web.Response(text=filepath, status=200)
|
||||
except Exception:
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@routes.post("/v2/manager/component/loads")
|
||||
async def load_components(request):
|
||||
if os.path.exists(context.manager_components_path):
|
||||
try:
|
||||
json_files = [f for f in os.listdir(context.manager_components_path) if f.endswith('.json')]
|
||||
pack_files = [f for f in os.listdir(context.manager_components_path) if f.endswith('.pack')]
|
||||
|
||||
components = {}
|
||||
for json_file in json_files + pack_files:
|
||||
file_path = os.path.join(context.manager_components_path, json_file)
|
||||
with open(file_path, 'r') as file:
|
||||
try:
|
||||
# When there is a conflict between the .pack and the .json, the pack takes precedence and overrides.
|
||||
components.update(json.load(file))
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"[ComfyUI-Manager] Error decoding component file in file {json_file}: {e}")
|
||||
|
||||
return web.json_response(components)
|
||||
except Exception as e:
|
||||
logging.error(f"[ComfyUI-Manager] failed to load components\n{e}")
|
||||
return web.Response(status=400)
|
||||
else:
|
||||
return web.json_response({})
|
||||
|
||||
|
||||
@routes.get("/v2/manager/version")
|
||||
async def get_version(request):
|
||||
return web.Response(text=core.version_str, status=200)
|
||||
@@ -1949,9 +1921,10 @@ if not os.path.exists(context.manager_config_path):
|
||||
core.write_config()
|
||||
|
||||
|
||||
cm_global.register_extension('ComfyUI-Manager',
|
||||
{'version': core.version,
|
||||
'name': 'ComfyUI Manager',
|
||||
'nodes': {},
|
||||
'description': 'This extension provides the ability to manage custom nodes in ComfyUI.', })
|
||||
|
||||
# policy setup
|
||||
manager_security.add_handler_policy(reinstall_custom_node, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
manager_security.add_handler_policy(install_custom_node, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
manager_security.add_handler_policy(fix_custom_node, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
manager_security.add_handler_policy(install_custom_node_git_url, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
manager_security.add_handler_policy(install_custom_node_pip, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
manager_security.add_handler_policy(install_model, manager_security.HANDLER_POLICY.MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD)
|
||||
|
||||
@@ -10,6 +10,16 @@ import hashlib
|
||||
|
||||
import folder_paths
|
||||
from server import PromptServer
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
try:
|
||||
from nio import AsyncClient, LoginResponse, UploadResponse
|
||||
matrix_nio_is_available = True
|
||||
except Exception:
|
||||
logging.warning(f"[ComfyUI-Manager] The matrix sharing feature has been disabled because the `matrix-nio` dependency is not installed.\n\tTo use this feature, please run the following command:\n\t{sys.executable} -m pip install matrix-nio\n")
|
||||
matrix_nio_is_available = False
|
||||
|
||||
|
||||
def extract_model_file_names(json_data):
|
||||
@@ -192,6 +202,14 @@ async def get_esheep_workflow_and_images(request):
|
||||
return web.Response(status=200, text=json.dumps(data))
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/v2/manager/get_matrix_dep_status")
|
||||
async def get_matrix_dep_status(request):
|
||||
if matrix_nio_is_available:
|
||||
return web.Response(status=200, text='available')
|
||||
else:
|
||||
return web.Response(status=200, text='unavailable')
|
||||
|
||||
|
||||
def set_matrix_auth(json_data):
|
||||
homeserver = json_data['homeserver']
|
||||
username = json_data['username']
|
||||
@@ -331,15 +349,12 @@ async def share_art(request):
|
||||
workflowId = upload_workflow_json["workflowId"]
|
||||
|
||||
# check if the user has provided Matrix credentials
|
||||
if "matrix" in share_destinations:
|
||||
if matrix_nio_is_available and "matrix" in share_destinations:
|
||||
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
|
||||
filename = os.path.basename(asset_filepath)
|
||||
content_type = assetFileType
|
||||
|
||||
try:
|
||||
from matrix_client.api import MatrixHttpApi
|
||||
from matrix_client.client import MatrixClient
|
||||
|
||||
homeserver = 'matrix.org'
|
||||
if matrix_auth:
|
||||
homeserver = matrix_auth.get('homeserver', 'matrix.org')
|
||||
@@ -347,20 +362,35 @@ async def share_art(request):
|
||||
if not homeserver.startswith("https://"):
|
||||
homeserver = "https://" + homeserver
|
||||
|
||||
client = MatrixClient(homeserver)
|
||||
try:
|
||||
token = client.login(username=matrix_auth['username'], password=matrix_auth['password'])
|
||||
if not token:
|
||||
return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
|
||||
except Exception:
|
||||
client = AsyncClient(homeserver, matrix_auth['username'])
|
||||
|
||||
# Login
|
||||
login_resp = await client.login(matrix_auth['password'])
|
||||
if not isinstance(login_resp, LoginResponse) or not login_resp.access_token:
|
||||
await client.close()
|
||||
return web.json_response({"error": "Invalid Matrix credentials."}, content_type='application/json', status=400)
|
||||
|
||||
matrix = MatrixHttpApi(homeserver, token=token)
|
||||
# Upload asset
|
||||
with open(asset_filepath, 'rb') as f:
|
||||
mxc_url = matrix.media_upload(f.read(), content_type, filename=filename)['content_uri']
|
||||
upload_resp, _maybe_keys = await client.upload(f, content_type=content_type, filename=filename)
|
||||
asset_data = f.seek(0) or f.read() # get size for info below
|
||||
if not isinstance(upload_resp, UploadResponse) or not upload_resp.content_uri:
|
||||
await client.close()
|
||||
return web.json_response({"error": "Failed to upload asset to Matrix."}, content_type='application/json', status=500)
|
||||
mxc_url = upload_resp.content_uri
|
||||
|
||||
workflow_json_mxc_url = matrix.media_upload(prompt['workflow'], 'application/json', filename='workflow.json')['content_uri']
|
||||
# Upload workflow JSON
|
||||
import io
|
||||
workflow_json_bytes = json.dumps(prompt['workflow']).encode('utf-8')
|
||||
workflow_io = io.BytesIO(workflow_json_bytes)
|
||||
upload_workflow_resp, _maybe_keys = await client.upload(workflow_io, content_type='application/json', filename='workflow.json')
|
||||
workflow_io.seek(0)
|
||||
if not isinstance(upload_workflow_resp, UploadResponse) or not upload_workflow_resp.content_uri:
|
||||
await client.close()
|
||||
return web.json_response({"error": "Failed to upload workflow to Matrix."}, content_type='application/json', status=500)
|
||||
workflow_json_mxc_url = upload_workflow_resp.content_uri
|
||||
|
||||
# Send text message
|
||||
text_content = ""
|
||||
if title:
|
||||
text_content += f"{title}\n"
|
||||
@@ -368,10 +398,45 @@ async def share_art(request):
|
||||
text_content += f"{description}\n"
|
||||
if credits:
|
||||
text_content += f"\ncredits: {credits}\n"
|
||||
matrix.send_message(comfyui_share_room_id, text_content)
|
||||
matrix.send_content(comfyui_share_room_id, mxc_url, filename, 'm.image')
|
||||
matrix.send_content(comfyui_share_room_id, workflow_json_mxc_url, 'workflow.json', 'm.file')
|
||||
except Exception:
|
||||
await client.room_send(
|
||||
room_id=comfyui_share_room_id,
|
||||
message_type="m.room.message",
|
||||
content={"msgtype": "m.text", "body": text_content}
|
||||
)
|
||||
|
||||
# Send image
|
||||
await client.room_send(
|
||||
room_id=comfyui_share_room_id,
|
||||
message_type="m.room.message",
|
||||
content={
|
||||
"msgtype": "m.image",
|
||||
"body": filename,
|
||||
"url": mxc_url,
|
||||
"info": {
|
||||
"mimetype": content_type,
|
||||
"size": len(asset_data)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Send workflow JSON file
|
||||
await client.room_send(
|
||||
room_id=comfyui_share_room_id,
|
||||
message_type="m.room.message",
|
||||
content={
|
||||
"msgtype": "m.file",
|
||||
"body": "workflow.json",
|
||||
"url": workflow_json_mxc_url,
|
||||
"info": {
|
||||
"mimetype": "application/json",
|
||||
"size": len(workflow_json_bytes)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await client.close()
|
||||
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return web.json_response({"error": "An error occurred when sharing your art to Matrix."}, content_type='application/json', status=500)
|
||||
|
||||
@@ -1973,6 +1973,97 @@
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
|
||||
"size": "375.0MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "sam2.1_hiera_tiny.pt",
|
||||
"type": "sam2.1",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2.1 hiera model (tiny)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2.1_hiera_tiny.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_tiny.pt",
|
||||
"size": "149.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2.1_hiera_small.pt",
|
||||
"type": "sam2.1",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2.1 hiera model (small)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2.1_hiera_small.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_small.pt",
|
||||
"size": "176.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2.1_hiera_base_plus.pt",
|
||||
"type": "sam2.1",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2.1 hiera model (base+)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2.1_hiera_base_plus.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_base_plus.pt",
|
||||
"size": "309.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2.1_hiera_large.pt",
|
||||
"type": "sam2.1",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2.1 hiera model (large)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2.1_hiera_large.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_large.pt",
|
||||
"size": "857.0MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "sam2_hiera_tiny.pt",
|
||||
"type": "sam2",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2 hiera model (tiny)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2_hiera_tiny.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_tiny.pt",
|
||||
"size": "149.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2_hiera_small.pt",
|
||||
"type": "sam2",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2 hiera model (small)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2_hiera_small.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_small.pt",
|
||||
"size": "176.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2_hiera_base_plus.pt",
|
||||
"type": "sam2",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2 hiera model (base+)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2_hiera_base_plus.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_base_plus.pt",
|
||||
"size": "309.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2_hiera_large.pt",
|
||||
"type": "sam2",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2 hiera model (large)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2_hiera_large.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_large.pt",
|
||||
"size": "857.0MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "seecoder v1.0",
|
||||
"type": "seecoder",
|
||||
@@ -4006,6 +4097,29 @@
|
||||
"size": "649MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Comfy-Org/omnigen2_fp16.safetensors",
|
||||
"type": "diffusion_model",
|
||||
"base": "OmniGen2",
|
||||
"save_path": "default",
|
||||
"description": "OmniGen2 diffusion model. This is required for using OmniGen2.",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged",
|
||||
"filename": "omnigen2_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/omnigen2_fp16.safetensors",
|
||||
"size": "7.93GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/qwen_2.5_vl_fp16.safetensors",
|
||||
"type": "clip",
|
||||
"base": "qwen-2.5",
|
||||
"save_path": "default",
|
||||
"description": "text encoder for OmniGen2",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged",
|
||||
"filename": "qwen_2.5_vl_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged/resolve/main/split_files/text_encoders/qwen_2.5_vl_fp16.safetensors",
|
||||
"size": "7.51GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "FLUX.1 [Schnell] Diffusion model",
|
||||
"type": "diffusion_model",
|
||||
@@ -4023,7 +4137,7 @@
|
||||
"type": "VAE",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "vae/FLUX1",
|
||||
"description": "FLUX.1 VAE model",
|
||||
"description": "FLUX.1 VAE model\nNOTE: This VAE model can also be used for image generation with OmniGen2.",
|
||||
"reference": "https://huggingface.co/black-forest-labs/FLUX.1-schnell",
|
||||
"filename": "ae.safetensors",
|
||||
"url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors",
|
||||
@@ -4931,6 +5045,105 @@
|
||||
"size": "1.26GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 i2v high noise 14B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for i2v high noise 14B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_i2v_high_noise_14B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp16.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 i2v high noise 14B (fp8_scaled)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for i2v high noise 14B (fp8_scaled)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"size": "14.3GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 i2v low noise 14B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for i2v low noise 14B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_i2v_low_noise_14B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp16.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 i2v low noise 14B (fp8_scaled)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for i2v low noise 14B (fp8_scaled)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"size": "14.3GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 t2v high noise 14B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for t2v high noise 14B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_t2v_high_noise_14B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp16.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 t2v high noise 14B (fp8_scaled)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for t2v high noise 14B (fp8_scaled)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"size": "14.3GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 t2v low noise 14B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for t2v low noise 14B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_t2v_low_noise_14B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp16.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 t2v low noise 14B (fp8_scaled)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for t2v low noise 14B (fp8_scaled)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"size": "14.3GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 ti2v 5B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for ti2v 5B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_ti2v_5B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_ti2v_5B_fp16.safetensors",
|
||||
"size": "10.0GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Comfy-Org/umt5_xxl_fp16.safetensors",
|
||||
@@ -5033,6 +5246,50 @@
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-fp8.safetensors",
|
||||
"size": "15.7GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 2B Distilled v0.9.8",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "LTX-Video 2B distilled model v0.9.8 with improved prompt understanding and detail generation.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-2b-0.9.8-distilled.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-2b-0.9.8-distilled.safetensors",
|
||||
"size": "6.34GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 2B Distilled FP8 v0.9.8",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Quantized LTX-Video 2B distilled model v0.9.8 with improved prompt understanding and detail generation, optimized for lower VRAM usage.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-2b-0.9.8-distilled-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-2b-0.9.8-distilled-fp8.safetensors",
|
||||
"size": "4.46GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B Distilled v0.9.8",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "LTX-Video 13B distilled model v0.9.8 with improved prompt understanding and detail generation.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.8-distilled.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.8-distilled.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B Distilled FP8 v0.9.8",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "Quantized LTX-Video 13B distilled model v0.9.8 with improved prompt understanding and detail generation, optimized for lower VRAM usage.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltxv-13b-0.9.8-distilled-fp8.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.8-distilled-fp8.safetensors",
|
||||
"size": "15.7GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video 13B Distilled LoRA v0.9.7",
|
||||
"type": "lora",
|
||||
@@ -5044,6 +5301,50 @@
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltxv-13b-0.9.7-distilled-lora128.safetensors",
|
||||
"size": "1.33GB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video ICLoRA Depth 13B v0.9.7",
|
||||
"type": "lora",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "loras",
|
||||
"description": "In-Context LoRA (IC LoRA) for depth-controlled video-to-video generation with precise depth conditioning.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-depth-13b-0.9.7",
|
||||
"filename": "ltxv-097-ic-lora-depth-control-comfyui.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-depth-13b-0.9.7/resolve/main/ltxv-097-ic-lora-depth-control-comfyui.safetensors",
|
||||
"size": "81.9MB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video ICLoRA Pose 13B v0.9.7",
|
||||
"type": "lora",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "loras",
|
||||
"description": "In-Context LoRA (IC LoRA) for pose-controlled video-to-video generation with precise pose conditioning.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-pose-13b-0.9.7",
|
||||
"filename": "ltxv-097-ic-lora-pose-control-comfyui.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-pose-13b-0.9.7/resolve/main/ltxv-097-ic-lora-pose-control-comfyui.safetensors",
|
||||
"size": "151MB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video ICLoRA Canny 13B v0.9.7",
|
||||
"type": "lora",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "loras",
|
||||
"description": "In-Context LoRA (IC LoRA) for canny edge-controlled video-to-video generation with precise edge conditioning.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-canny-13b-0.9.7",
|
||||
"filename": "ltxv-097-ic-lora-canny-control-comfyui.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-canny-13b-0.9.7/resolve/main/ltxv-097-ic-lora-canny-control-comfyui.safetensors",
|
||||
"size": "81.9MB"
|
||||
},
|
||||
{
|
||||
"name": "LTX-Video ICLoRA Detailer 13B v0.9.8",
|
||||
"type": "lora",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "loras",
|
||||
"description": "A video detailer model on top of LTXV_13B_098_DEV trained on custom data using In-Context LoRA (IC LoRA) method.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-detailer-13b-0.9.8",
|
||||
"filename": "ltxv-098-ic-lora-detailer-comfyui.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video-ICLoRA-detailer-13b-0.9.8/resolve/main/ltxv-098-ic-lora-detailer-comfyui.safetensors",
|
||||
"size": "1.31GB"
|
||||
},
|
||||
{
|
||||
"name": "Latent Bridge Matching for Image Relighting",
|
||||
"type": "diffusion_model",
|
||||
@@ -5054,6 +5355,317 @@
|
||||
"filename": "LBM_relighting.safetensors",
|
||||
"url": "https://huggingface.co/jasperai/LBM_relighting/resolve/main/model.safetensors",
|
||||
"size": "5.02GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image VAE",
|
||||
"type": "VAE",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "vae/qwen-image",
|
||||
"description": "VAE model for Qwen-Image",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
|
||||
"filename": "qwen_image_vae.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors",
|
||||
"size": "335MB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen 2.5 VL 7B Text Encoder (fp8_scaled)",
|
||||
"type": "clip",
|
||||
"base": "Qwen-2.5-VL",
|
||||
"save_path": "text_encoders/qwen",
|
||||
"description": "Qwen 2.5 VL 7B text encoder model (fp8_scaled)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
|
||||
"filename": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b_fp8_scaled.safetensors",
|
||||
"size": "3.75GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen 2.5 VL 7B Text Encoder",
|
||||
"type": "clip",
|
||||
"base": "Qwen-2.5-VL",
|
||||
"save_path": "text_encoders/qwen",
|
||||
"description": "Qwen 2.5 VL 7B text encoder model",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
|
||||
"filename": "qwen_2.5_vl_7b.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/text_encoders/qwen_2.5_vl_7b.safetensors",
|
||||
"size": "7.51GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image Diffusion Model (fp8_e4m3fn)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "diffusion_models/qwen-image",
|
||||
"description": "Qwen-Image diffusion model (fp8_e4m3fn)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
|
||||
"filename": "qwen_image_fp8_e4m3fn.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_fp8_e4m3fn.safetensors",
|
||||
"size": "4.89GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image Diffusion Model (bf16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "diffusion_models/qwen-image",
|
||||
"description": "Qwen-Image diffusion model (bf16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI",
|
||||
"filename": "qwen_image_bf16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_bf16.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Edit 2509 Diffusion Model (fp8_e4m3fn)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "diffusion_models/qwen-image-edit",
|
||||
"description": "Qwen-Image-Edit 2509 diffusion model (fp8_e4m3fn)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI",
|
||||
"filename": "qwen_image_edit_2509_fp8_e4m3fn.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_2509_fp8_e4m3fn.safetensors",
|
||||
"size": "4.89GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Qwen-Image-Edit 2509 Diffusion Model (bf16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "diffusion_models/qwen-image-edit",
|
||||
"description": "Qwen-Image-Edit 2509 diffusion model (bf16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI",
|
||||
"filename": "qwen_image_edit_2509_bf16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_2509_bf16.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Qwen-Image-Edit Diffusion Model (fp8_e4m3fn)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "diffusion_models/qwen-image-edit",
|
||||
"description": "Qwen-Image-Edit diffusion model (fp8_e4m3fn)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI",
|
||||
"filename": "qwen_image_edit_fp8_e4m3fn.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_fp8_e4m3fn.safetensors",
|
||||
"size": "4.89GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Qwen-Image-Edit Diffusion Model (bf16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "diffusion_models/qwen-image-edit",
|
||||
"description": "Qwen-Image-Edit diffusion model (bf16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI",
|
||||
"filename": "qwen_image_edit_bf16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_bf16.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Lightning 8steps V1.0",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "loras/qwen-image-lightning",
|
||||
"description": "Qwen-Image-Lightning 8-step LoRA model V1.0",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Lightning-8steps-V1.0.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V1.0.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Lightning 4steps V1.0",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "loras/qwen-image-lightning",
|
||||
"description": "Qwen-Image-Lightning 4-step LoRA model V1.0",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Lightning-4steps-V1.0.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-4steps-V1.0.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Lightning 4steps V1.0 (bf16)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "loras/qwen-image-lightning",
|
||||
"description": "Qwen-Image-Lightning 4-step LoRA model V1.0 (bf16)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Lightning-4steps-V1.0-bf16.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-4steps-V1.0-bf16.safetensors",
|
||||
"size": "19.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Lightning 4steps V2.0",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "loras/qwen-image-lightning",
|
||||
"description": "Qwen-Image-Lightning 4-step LoRA model V2.0",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Lightning-4steps-V2.0.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-4steps-V2.0.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Lightning 4steps V2.0 (bf16)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "loras/qwen-image-lightning",
|
||||
"description": "Qwen-Image-Lightning 4-step LoRA model V2.0 (bf16)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Lightning-4steps-V2.0-bf16.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-4steps-V2.0-bf16.safetensors",
|
||||
"size": "19.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Lightning 8steps V1.1",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "loras/qwen-image-lightning",
|
||||
"description": "Qwen-Image-Lightning 8-step LoRA model V1.1",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Lightning-8steps-V1.1.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V1.1.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Lightning 8steps V1.1 (bf16)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "loras/qwen-image-lightning",
|
||||
"description": "Qwen-Image-Lightning 8-step LoRA model V1.1 (bf16)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Lightning-8steps-V1.1-bf16.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V1.1-bf16.safetensors",
|
||||
"size": "19.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Lightning 8steps V2.0",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "loras/qwen-image-lightning",
|
||||
"description": "Qwen-Image-Lightning 8-step LoRA model V2.0",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Lightning-8steps-V2.0.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V2.0.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Lightning 8steps V2.0 (bf16)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "loras/qwen-image-lightning",
|
||||
"description": "Qwen-Image-Lightning 8-step LoRA model V2.0 (bf16)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Lightning-8steps-V2.0-bf16.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V2.0-bf16.safetensors",
|
||||
"size": "19.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Edit-Lightning 4steps V1.0",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "loras/qwen-image-edit-lightning",
|
||||
"description": "Qwen-Image-Edit-Lightning 4-step LoRA model V1.0",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Edit-Lightning-4steps-V1.0.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-Lightning-4steps-V1.0.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Edit-Lightning 4steps V1.0 (bf16)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "loras/qwen-image-edit-lightning",
|
||||
"description": "Qwen-Image-Edit-Lightning 4-step LoRA model V1.0 (bf16)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Edit-Lightning-4steps-V1.0-bf16.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-Lightning-4steps-V1.0-bf16.safetensors",
|
||||
"size": "19.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Edit-Lightning 8steps V1.0",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "loras/qwen-image-edit-lightning",
|
||||
"description": "Qwen-Image-Edit-Lightning 8-step LoRA model V1.0",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Edit-Lightning-8steps-V1.0.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-Lightning-8steps-V1.0.safetensors",
|
||||
"size": "9.78GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Edit-Lightning 8steps V1.0 (bf16)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "loras/qwen-image-edit-lightning",
|
||||
"description": "Qwen-Image-Edit-Lightning 8-step LoRA model V1.0 (bf16)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Edit-Lightning-8steps-V1.0-bf16.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-Lightning-8steps-V1.0-bf16.safetensors",
|
||||
"size": "19.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Edit-2509-Lightning 4steps V1.0 (bf16)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "loras/qwen-image-edit-lightning",
|
||||
"description": "Qwen-Image-Edit-2509-Lightning 4-step LoRA model V1.0 (bf16)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16.safetensors",
|
||||
"size": "19.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Edit-2509-Lightning 4steps V1.0 (fp32)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "loras/qwen-image-edit-lightning",
|
||||
"description": "Qwen-Image-Edit-2509-Lightning 4-step LoRA model V1.0 (fp32)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Edit-2509-Lightning-4steps-V1.0-fp32.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-fp32.safetensors",
|
||||
"size": "39.1GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Edit-2509-Lightning 8steps V1.0 (bf16)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "loras/qwen-image-edit-lightning",
|
||||
"description": "Qwen-Image-Edit-2509-Lightning 8-step LoRA model V1.0 (bf16)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Edit-2509-Lightning-8steps-V1.0-bf16.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-8steps-V1.0-bf16.safetensors",
|
||||
"size": "19.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image-Edit-2509-Lightning 8steps V1.0 (fp32)",
|
||||
"type": "lora",
|
||||
"base": "Qwen-Image-Edit",
|
||||
"save_path": "loras/qwen-image-edit-lightning",
|
||||
"description": "Qwen-Image-Edit-2509-Lightning 8-step LoRA model V1.0 (fp32)",
|
||||
"reference": "https://huggingface.co/lightx2v/Qwen-Image-Lightning",
|
||||
"filename": "Qwen-Image-Edit-2509-Lightning-8steps-V1.0-fp32.safetensors",
|
||||
"url": "https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-8steps-V1.0-fp32.safetensors",
|
||||
"size": "39.1GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image InstantX ControlNet Union",
|
||||
"type": "controlnet",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "controlnet/qwen-image/instantx",
|
||||
"description": "Qwen-Image InstantX ControlNet Union model",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets",
|
||||
"filename": "Qwen-Image-InstantX-ControlNet-Union.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets/resolve/main/split_files/controlnet/Qwen-Image-InstantX-ControlNet-Union.safetensors",
|
||||
"size": "2.54GB"
|
||||
},
|
||||
{
|
||||
"name": "Qwen-Image InstantX ControlNet Inpainting",
|
||||
"type": "controlnet",
|
||||
"base": "Qwen-Image",
|
||||
"save_path": "controlnet/qwen-image/instantx",
|
||||
"description": "Qwen-Image InstantX ControlNet Inpainting model",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets",
|
||||
"filename": "Qwen-Image-InstantX-ControlNet-Inpainting.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets/resolve/main/split_files/controlnet/Qwen-Image-InstantX-ControlNet-Inpainting.safetensors",
|
||||
"size": "2.54GB"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -16,26 +16,11 @@ from .common import security_check
|
||||
from .common import manager_util
|
||||
from .common import cm_global
|
||||
from .common import manager_downloader
|
||||
from .common.timestamp_utils import current_timestamp
|
||||
import folder_paths
|
||||
|
||||
manager_util.add_python_path_to_env()
|
||||
|
||||
import datetime as dt
|
||||
|
||||
if hasattr(dt, 'datetime'):
|
||||
from datetime import datetime as dt_datetime
|
||||
|
||||
def current_timestamp():
|
||||
return dt_datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
||||
else:
|
||||
# NOTE: Occurs in some Mac environments.
|
||||
import time
|
||||
logging.error(f"[ComfyUI-Manager] fallback timestamp mode\n datetime module is invalid: '{dt.__file__}'")
|
||||
|
||||
def current_timestamp():
|
||||
return str(time.time()).split('.')[0]
|
||||
|
||||
security_check.security_check()
|
||||
|
||||
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
|
||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||
@@ -81,7 +66,7 @@ cm_global.register_api('cm.is_import_failed_extension', is_import_failed_extensi
|
||||
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
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 = folder_paths.get_system_user_directory("manager")
|
||||
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")
|
||||
@@ -111,19 +96,14 @@ def check_file_logging():
|
||||
|
||||
read_config()
|
||||
read_uv_mode()
|
||||
security_check.security_check()
|
||||
check_file_logging()
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
cm_global.pip_overrides = {'numpy': 'numpy<2'}
|
||||
else:
|
||||
cm_global.pip_overrides = {}
|
||||
cm_global.pip_overrides = {}
|
||||
|
||||
if os.path.exists(manager_pip_overrides_path):
|
||||
with open(manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
cm_global.pip_overrides = json.load(json_file)
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
cm_global.pip_overrides['numpy'] = 'numpy<2'
|
||||
|
||||
|
||||
if os.path.exists(manager_pip_blacklist_path):
|
||||
@@ -489,7 +469,7 @@ check_bypass_ssl()
|
||||
|
||||
# Perform install
|
||||
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(manager_files_path, "startup-scripts", "install-scripts.txt")
|
||||
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -139,9 +139,9 @@ You can set whether to use ComfyUI-Manager solely via CLI.
|
||||
`restore-dependencies`
|
||||
|
||||
* This command can be used if custom nodes are installed under the `ComfyUI/custom_nodes` path but their dependencies are not installed.
|
||||
* It is useful when starting a new cloud instance, like colab, where dependencies need to be reinstalled and installation scripts re-executed.
|
||||
* It is useful when starting a new cloud instance, like Colab, where dependencies need to be reinstalled and installation scripts re-executed.
|
||||
* It can also be utilized if ComfyUI is reinstalled and only the custom_nodes path has been backed up and restored.
|
||||
|
||||
### 7. Clear
|
||||
|
||||
In the GUI, installations, updates, or snapshot restorations are scheduled to execute the next time ComfyUI is launched. The `clear` command clears this scheduled state, ensuring no pre-execution actions are applied.
|
||||
In the GUI, installations, updates, or snapshot restorations are scheduled to execute the next time ComfyUI is launched. The `clear` command clears this scheduled state, ensuring no pre-execution actions are applied.
|
||||
|
||||
+13
-13
@@ -23,13 +23,13 @@ OPTIONS:
|
||||
## How To Use?
|
||||
* `python cm-cli.py` 를 통해서 실행 시킬 수 있습니다.
|
||||
* 예를 들어 custom node를 모두 업데이트 하고 싶다면
|
||||
* ComfyUI-Manager경로 에서 `python cm-cli.py update all` 를 command를 실행할 수 있습니다.
|
||||
* ComfyUI-Manager 경로에서 `python cm-cli.py update all` 명령을 실행할 수 있습니다.
|
||||
* ComfyUI 경로에서 실행한다면, `python custom_nodes/ComfyUI-Manager/cm-cli.py update all` 와 같이 cm-cli.py 의 경로를 지정할 수도 있습니다.
|
||||
|
||||
## Prerequisite
|
||||
* ComfyUI 를 실행하는 python과 동일한 python 환경에서 실행해야 합니다.
|
||||
* venv를 사용할 경우 해당 venv를 activate 한 상태에서 실행해야 합니다.
|
||||
* portable 버전을 사용할 경우 run_nvidia_gpu.bat 파일이 있는 경로인 경우, 다음과 같은 방식으로 코맨드를 실행해야 합니다.
|
||||
* portable 버전을 사용할 경우 run_nvidia_gpu.bat 파일이 있는 경로인 경우, 다음과 같은 방식으로 명령을 실행해야 합니다.
|
||||
`.\python_embeded\python.exe ComfyUI\custom_nodes\ComfyUI-Manager\cm-cli.py update all`
|
||||
* ComfyUI 의 경로는 COMFYUI_PATH 환경 변수로 설정할 수 있습니다. 만약 생략할 경우 다음과 같은 경고 메시지가 나타나며, ComfyUI-Manager가 설치된 경로를 기준으로 상대 경로로 설정됩니다.
|
||||
```
|
||||
@@ -40,8 +40,8 @@ OPTIONS:
|
||||
|
||||
### 1. --channel, --mode
|
||||
* 정보 보기 기능과 커스텀 노드 관리 기능의 경우는 --channel과 --mode를 통해 정보 DB를 설정할 수 있습니다.
|
||||
* 예들 들어 `python cm-cli.py update all --channel recent --mode remote`와 같은 command를 실행할 경우, 현재 ComfyUI-Manager repo에 내장된 로컬의 정보가 아닌 remote의 최신 정보를 기준으로 동작하며, recent channel에 있는 목록을 대상으로만 동작합니다.
|
||||
* --channel, --mode 는 `simple-show, show, install, uninstall, update, disable, enable, fix` command에서만 사용 가능합니다.
|
||||
* 예를 들어 `python cm-cli.py update all --channel recent --mode remote`와 같은 명령을 실행할 경우, 현재 ComfyUI-Manager repo에 내장된 로컬의 정보가 아닌 remote의 최신 정보를 기준으로 동작하며, recent channel에 있는 목록을 대상으로만 동작합니다.
|
||||
* --channel, --mode 는 `simple-show, show, install, uninstall, update, disable, enable, fix` 명령에서만 사용 가능합니다.
|
||||
|
||||
### 2. 관리 정보 보기
|
||||
|
||||
@@ -51,7 +51,7 @@ OPTIONS:
|
||||
* `[show|simple-show]` - `show`는 상세하게 정보를 보여주며, `simple-show`는 간단하게 정보를 보여줍니다.
|
||||
|
||||
|
||||
`python cm-cli.py show installed` 와 같은 코맨드를 실행하면 설치된 커스텀 노드의 정보를 상세하게 보여줍니다.
|
||||
`python cm-cli.py show installed` 와 같은 명령을 실행하면 설치된 커스텀 노드의 정보를 상세하게 보여줍니다.
|
||||
```
|
||||
-= ComfyUI-Manager CLI (V2.24) =-
|
||||
|
||||
@@ -67,7 +67,7 @@ FETCH DATA from: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main
|
||||
[ DISABLED ] ComfyUI-Loopchain (author: Fannovel16)
|
||||
```
|
||||
|
||||
`python cm-cli.py simple-show installed` 와 같은 코맨드를 이용해서 설치된 커스텀 노드의 정보를 간단하게 보여줍니다.
|
||||
`python cm-cli.py simple-show installed` 와 같은 명령을 이용해서 설치된 커스텀 노드의 정보를 간단하게 보여줍니다.
|
||||
|
||||
```
|
||||
-= ComfyUI-Manager CLI (V2.24) =-
|
||||
@@ -89,7 +89,7 @@ ComfyUI-Loopchain
|
||||
* `installed`: enable, disable 여부와 상관없이 설치된 모든 노드를 보여줍니다
|
||||
* `not-installed`: 설치되지 않은 커스텀 노드의 목록을 보여줍니다.
|
||||
* `all`: 모든 커스텀 노드의 목록을 보여줍니다.
|
||||
* `snapshot`: 현재 설치된 커스텀 노드의 snapshot 정보를 보여줍니다. `show`롤 통해서 볼 경우는 json 출력 형태로 보여주며, `simple-show`를 통해서 볼 경우는 간단하게, 커밋 해시와 함께 보여줍니다.
|
||||
* `snapshot`: 현재 설치된 커스텀 노드의 snapshot 정보를 보여줍니다. `show`를 통해서 볼 경우는 json 출력 형태로 보여주며, `simple-show`를 통해서 볼 경우는 간단하게, 커밋 해시와 함께 보여줍니다.
|
||||
* `snapshot-list`: ComfyUI-Manager/snapshots 에 저장된 snapshot 파일의 목록을 보여줍니다.
|
||||
|
||||
### 3. 커스텀 노드 관리 하기
|
||||
@@ -98,7 +98,7 @@ ComfyUI-Loopchain
|
||||
|
||||
* `python cm-cli.py install ComfyUI-Impact-Pack ComfyUI-Inspire-Pack ComfyUI_experiments` 와 같이 커스텀 노드의 이름을 나열해서 관리 기능을 적용할 수 있습니다.
|
||||
* 커스텀 노드의 이름은 `show`를 했을 때 보여주는 이름이며, git repository의 이름입니다.
|
||||
(추후 nickname 을 사용가능하돌고 업데이트 할 예정입니다.)
|
||||
(추후 nickname을 사용 가능하도록 업데이트할 예정입니다.)
|
||||
|
||||
`[update|disable|enable|fix] all ?[--channel <channel name>] ?[--mode [remote|local|cache]]`
|
||||
|
||||
@@ -124,7 +124,7 @@ ComfyUI-Loopchain
|
||||
* `--pip-non-local-url`: web URL에 등록된 pip 패키지들에 대해서 복구를 수행
|
||||
* `--pip-local-url`: local 경로를 지정하고 있는 pip 패키지들에 대해서 복구를 수행
|
||||
* `--user-directory`: 사용자 디렉토리 설정
|
||||
* `--restore-to`: 복구될 커스텀 노드가 설치될 경로. (이 옵션을 적용할 경우 오직 대상 경로에 설치된 custom nodes 만 설치된 것으로 인식함.)
|
||||
* `--restore-to`: 복구될 커스텀 노드가 설치될 경로. (이 옵션을 적용할 경우 오직 대상 경로에 설치된 custom nodes만 설치된 것으로 인식함.)
|
||||
|
||||
### 5. CLI only mode
|
||||
|
||||
@@ -133,7 +133,7 @@ ComfyUI-Manager를 CLI로만 사용할 것인지를 설정할 수 있습니다.
|
||||
`cli-only-mode [enable|disable]`
|
||||
|
||||
* security 혹은 policy 의 이유로 GUI 를 통한 ComfyUI-Manager 사용을 제한하고 싶은 경우 이 모드를 사용할 수 있습니다.
|
||||
* CLI only mode를 적용할 경우 ComfyUI-Manager 가 매우 제한된 상태로 로드되어, 내부적으로 제공하는 web API가 비활성화 되며, 메인 메뉴에서도 Manager 버튼이 표시되지 않습니다.
|
||||
* CLI only mode를 적용할 경우 ComfyUI-Manager 가 매우 제한된 상태로 로드되어, 내부적으로 제공하는 web API가 비활성화되며, 메인 메뉴에서도 Manager 버튼이 표시되지 않습니다.
|
||||
|
||||
|
||||
### 6. 의존성 설치
|
||||
@@ -141,10 +141,10 @@ ComfyUI-Manager를 CLI로만 사용할 것인지를 설정할 수 있습니다.
|
||||
`restore-dependencies`
|
||||
|
||||
* `ComfyUI/custom_nodes` 하위 경로에 커스텀 노드들이 설치되어 있긴 하지만, 의존성이 설치되지 않은 경우 사용할 수 있습니다.
|
||||
* colab 과 같이 cloud instance를 새로 시작하는 경우 의존성 재설치 및 설치 스크립트가 재실행 되어야 하는 경우 사용합니다.
|
||||
* ComfyUI을 재설치할 경우, custom_nodes 경로만 백업했다가 재설치 할 경우 활용 가능합니다.
|
||||
* Colab과 같이 cloud instance를 새로 시작하는 경우 의존성 재설치 및 설치 스크립트가 재실행되어야 하는 경우 사용합니다.
|
||||
* ComfyUI를 재설치할 경우, custom_nodes 경로만 백업했다가 재설치할 경우 활용 가능합니다.
|
||||
|
||||
|
||||
### 7. clear
|
||||
|
||||
GUI에서 install, update를 하거나 snapshot 을 restore하는 경우 예약을 통해서 다음번 ComfyUI를 실행할 경우 실행되는 구조입니다. `clear` 는 이런 예약 상태를 clear해서, 아무런 사전 실행이 적용되지 않도록 합니다.
|
||||
GUI에서 install, update를 하거나 snapshot을 restore하는 경우 예약을 통해서 다음번 ComfyUI를 실행할 경우 실행되는 구조입니다. `clear` 는 이런 예약 상태를 clear해서, 아무런 사전 실행이 적용되지 않도록 합니다.
|
||||
|
||||
+3994
-727
File diff suppressed because it is too large
Load Diff
+5141
-1516
File diff suppressed because it is too large
Load Diff
+2752
-1092
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
rm ~/.tmp/dev/*.py > /dev/null 2>&1
|
||||
python ../../scanner.py ~/.tmp/dev
|
||||
python ../../scanner.py ~/.tmp/dev $*
|
||||
@@ -1,5 +1,25 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "synchronicity-labs",
|
||||
"title": "ComfyUI Sync Lipsync Node",
|
||||
"reference": "https://github.com/synchronicity-labs/sync-comfyui",
|
||||
"files": [
|
||||
"https://github.com/synchronicity-labs/sync-comfyui"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This custom node allows you to perform audio-video lip synchronization inside ComfyUI using a simple interface."
|
||||
},
|
||||
{
|
||||
"author": "joaomede",
|
||||
"title": "ComfyUI-Unload-Model-Fork",
|
||||
"reference": "https://github.com/joaomede/ComfyUI-Unload-Model-Fork",
|
||||
"files": [
|
||||
"https://github.com/joaomede/ComfyUI-Unload-Model-Fork"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "For unloading a model or all models, using the memory management that is already present in ComfyUI. Copied from [a/https://github.com/willblaschko/ComfyUI-Unload-Models](https://github.com/willblaschko/ComfyUI-Unload-Models) but without the unnecessary extra stuff."
|
||||
},
|
||||
{
|
||||
"author": "SanDiegoDude",
|
||||
"title": "ComfyUI-HiDream-Sampler [WIP]",
|
||||
@@ -149,6 +169,16 @@
|
||||
],
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"author": "huixingyun",
|
||||
"title": "ComfyUI-SoundFlow",
|
||||
"reference": "https://github.com/huixingyun/ComfyUI-SoundFlow",
|
||||
"files": [
|
||||
"https://github.com/huixingyun/ComfyUI-SoundFlow"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "forked from https://github.com/fredconex/ComfyUI-SoundFlow (removed)"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+393
-390
File diff suppressed because it is too large
Load Diff
+15370
-1610
File diff suppressed because it is too large
Load Diff
+214
-218
@@ -1,5 +1,219 @@
|
||||
{
|
||||
"models": [
|
||||
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 i2v high noise 14B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for i2v high noise 14B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_i2v_high_noise_14B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp16.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 i2v high noise 14B (fp8_scaled)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for i2v high noise 14B (fp8_scaled)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"size": "14.3GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 i2v low noise 14B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for i2v low noise 14B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_i2v_low_noise_14B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp16.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 i2v low noise 14B (fp8_scaled)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for i2v low noise 14B (fp8_scaled)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"size": "14.3GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 t2v high noise 14B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for t2v high noise 14B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_t2v_high_noise_14B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp16.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 t2v high noise 14B (fp8_scaled)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for t2v high noise 14B (fp8_scaled)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"size": "14.3GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 t2v low noise 14B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for t2v low noise 14B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_t2v_low_noise_14B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp16.safetensors",
|
||||
"size": "28.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 t2v low noise 14B (fp8_scaled)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for t2v low noise 14B (fp8_scaled)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"size": "14.3GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/Wan2.2 ti2v 5B (fp16)",
|
||||
"type": "diffusion_model",
|
||||
"base": "Wan2.2",
|
||||
"save_path": "diffusion_models/Wan2.2",
|
||||
"description": "Wan2.2 diffusion model for ti2v 5B (fp16)",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"filename": "wan2.2_ti2v_5B_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_ti2v_5B_fp16.safetensors",
|
||||
"size": "10.0GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "sam2.1_hiera_tiny.pt",
|
||||
"type": "sam2.1",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2.1 hiera model (tiny)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2.1_hiera_tiny.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_tiny.pt",
|
||||
"size": "149.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2.1_hiera_small.pt",
|
||||
"type": "sam2.1",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2.1 hiera model (small)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2.1_hiera_small.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_small.pt",
|
||||
"size": "176.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2.1_hiera_base_plus.pt",
|
||||
"type": "sam2.1",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2.1 hiera model (base+)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2.1_hiera_base_plus.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_base_plus.pt",
|
||||
"size": "309.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2.1_hiera_large.pt",
|
||||
"type": "sam2.1",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2.1 hiera model (large)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2.1_hiera_large.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_large.pt",
|
||||
"size": "857.0MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "sam2_hiera_tiny.pt",
|
||||
"type": "sam2",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2 hiera model (tiny)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2_hiera_tiny.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_tiny.pt",
|
||||
"size": "149.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2_hiera_small.pt",
|
||||
"type": "sam2",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2 hiera model (small)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2_hiera_small.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_small.pt",
|
||||
"size": "176.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2_hiera_base_plus.pt",
|
||||
"type": "sam2",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2 hiera model (base+)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2_hiera_base_plus.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_base_plus.pt",
|
||||
"size": "309.0MB"
|
||||
},
|
||||
{
|
||||
"name": "sam2_hiera_large.pt",
|
||||
"type": "sam2",
|
||||
"base": "SAM",
|
||||
"save_path": "sams",
|
||||
"description": "Segmenty Anything SAM 2 hiera model (large)",
|
||||
"reference": "https://github.com/facebookresearch/sam2#model-description",
|
||||
"filename": "sam2_hiera_large.pt",
|
||||
"url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_large.pt",
|
||||
"size": "857.0MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Comfy-Org/omnigen2_fp16.safetensors",
|
||||
"type": "diffusion_model",
|
||||
"base": "OmniGen2",
|
||||
"save_path": "default",
|
||||
"description": "OmniGen2 diffusion model. This is required for using OmniGen2.",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged",
|
||||
"filename": "omnigen2_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/omnigen2_fp16.safetensors",
|
||||
"size": "7.93GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/qwen_2.5_vl_fp16.safetensors",
|
||||
"type": "clip",
|
||||
"base": "qwen-2.5",
|
||||
"save_path": "default",
|
||||
"description": "text encoder for OmniGen2",
|
||||
"reference": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged",
|
||||
"filename": "qwen_2.5_vl_fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged/resolve/main/split_files/text_encoders/qwen_2.5_vl_fp16.safetensors",
|
||||
"size": "7.51GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Latent Bridge Matching for Image Relighting",
|
||||
"type": "diffusion_model",
|
||||
@@ -473,224 +687,6 @@
|
||||
"filename": "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"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "PixArt-Sigma-XL-2-512-MS.safetensors (diffusion)",
|
||||
"type": "diffusion_model",
|
||||
"base": "pixart-sigma",
|
||||
"save_path": "diffusion_models/PixArt-Sigma",
|
||||
"description": "PixArt-Sigma Diffusion model",
|
||||
"reference": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-512-MS",
|
||||
"filename": "PixArt-Sigma-XL-2-512-MS.safetensors",
|
||||
"url": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-512-MS/resolve/main/transformer/diffusion_pytorch_model.safetensors",
|
||||
"size": "2.44GB"
|
||||
},
|
||||
{
|
||||
"name": "PixArt-Sigma-XL-2-1024-MS.safetensors (diffusion)",
|
||||
"type": "diffusion_model",
|
||||
"base": "pixart-sigma",
|
||||
"save_path": "diffusion_models/PixArt-Sigma",
|
||||
"description": "PixArt-Sigma Diffusion model",
|
||||
"reference": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS",
|
||||
"filename": "PixArt-Sigma-XL-2-1024-MS.safetensors",
|
||||
"url": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS/resolve/main/transformer/diffusion_pytorch_model.safetensors",
|
||||
"size": "2.44GB"
|
||||
},
|
||||
{
|
||||
"name": "PixArt-XL-2-1024-MS.safetensors (diffusion)",
|
||||
"type": "diffusion_model",
|
||||
"base": "pixart-alpha",
|
||||
"save_path": "diffusion_models/PixArt-Alpha",
|
||||
"description": "PixArt-Alpha Diffusion model",
|
||||
"reference": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS",
|
||||
"filename": "PixArt-XL-2-1024-MS.safetensors",
|
||||
"url": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS/resolve/main/transformer/diffusion_pytorch_model.safetensors",
|
||||
"size": "2.45GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Comfy-Org/hunyuan_video_t2v_720p_bf16.safetensors",
|
||||
"type": "diffusion_model",
|
||||
"base": "Hunyuan Video",
|
||||
"save_path": "diffusion_models/hunyuan_video",
|
||||
"description": "Huyuan Video diffusion model. repackaged version.",
|
||||
"reference": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged",
|
||||
"filename": "hunyuan_video_t2v_720p_bf16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/diffusion_models/hunyuan_video_t2v_720p_bf16.safetensors",
|
||||
"size": "25.6GB"
|
||||
},
|
||||
{
|
||||
"name": "Comfy-Org/hunyuan_video_vae_bf16.safetensors",
|
||||
"type": "VAE",
|
||||
"base": "Hunyuan Video",
|
||||
"save_path": "VAE",
|
||||
"description": "Huyuan Video VAE model. repackaged version.",
|
||||
"reference": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged",
|
||||
"filename": "hunyuan_video_vae_bf16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/vae/hunyuan_video_vae_bf16.safetensors",
|
||||
"size": "493MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "LTX-Video 2B v0.9.1 Checkpoint",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "LTX-Video is the first DiT-based video generation model capable of generating high-quality videos in real-time. It produces 24 FPS videos at a 768x512 resolution faster than they can be watched. Trained on a large-scale dataset of diverse videos, the model generates high-resolution videos with realistic and varied content.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltx-video-2b-v0.9.1.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.1.safetensors",
|
||||
"size": "5.72GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "XLabs-AI/flux-canny-controlnet-v3.safetensors",
|
||||
"type": "controlnet",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "xlabs/controlnets",
|
||||
"description": "ControlNet checkpoints for FLUX.1-dev model by Black Forest Labs.",
|
||||
"reference": "https://huggingface.co/XLabs-AI/flux-controlnet-collections",
|
||||
"filename": "flux-canny-controlnet-v3.safetensors",
|
||||
"url": "https://huggingface.co/XLabs-AI/flux-controlnet-collections/resolve/main/flux-canny-controlnet-v3.safetensors",
|
||||
"size": "1.49GB"
|
||||
},
|
||||
{
|
||||
"name": "XLabs-AI/flux-depth-controlnet-v3.safetensors",
|
||||
"type": "controlnet",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "xlabs/controlnets",
|
||||
"description": "ControlNet checkpoints for FLUX.1-dev model by Black Forest Labs.",
|
||||
"reference": "https://huggingface.co/XLabs-AI/flux-controlnet-collections",
|
||||
"filename": "flux-depth-controlnet-v3.safetensors",
|
||||
"url": "https://huggingface.co/XLabs-AI/flux-controlnet-collections/resolve/main/flux-depth-controlnet-v3.safetensors",
|
||||
"size": "1.49GB"
|
||||
},
|
||||
{
|
||||
"name": "XLabs-AI/flux-hed-controlnet-v3.safetensors",
|
||||
"type": "controlnet",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "xlabs/controlnets",
|
||||
"description": "ControlNet checkpoints for FLUX.1-dev model by Black Forest Labs.",
|
||||
"reference": "https://huggingface.co/XLabs-AI/flux-controlnet-collections",
|
||||
"filename": "flux-hed-controlnet-v3.safetensors",
|
||||
"url": "https://huggingface.co/XLabs-AI/flux-controlnet-collections/resolve/main/flux-hed-controlnet-v3.safetensors",
|
||||
"size": "1.49GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "XLabs-AI/realism_lora.safetensors",
|
||||
"type": "lora",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "xlabs/loras",
|
||||
"description": "A checkpoint with trained LoRAs for FLUX.1-dev model by Black Forest Labs",
|
||||
"reference": "https://huggingface.co/XLabs-AI/flux-lora-collection",
|
||||
"filename": "realism_lora.safetensors",
|
||||
"url": "https://huggingface.co/XLabs-AI/flux-lora-collection/resolve/main/realism_lora.safetensors",
|
||||
"size": "44.8MB"
|
||||
},
|
||||
{
|
||||
"name": "XLabs-AI/art_lora.safetensors",
|
||||
"type": "lora",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "xlabs/loras",
|
||||
"description": "A checkpoint with trained LoRAs for FLUX.1-dev model by Black Forest Labs",
|
||||
"reference": "https://huggingface.co/XLabs-AI/flux-lora-collection",
|
||||
"filename": "art_lora.safetensors",
|
||||
"url": "https://huggingface.co/XLabs-AI/flux-lora-collection/resolve/main/scenery_lora.safetensors",
|
||||
"size": "44.8MB"
|
||||
},
|
||||
{
|
||||
"name": "XLabs-AI/mjv6_lora.safetensors",
|
||||
"type": "lora",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "xlabs/loras",
|
||||
"description": "A checkpoint with trained LoRAs for FLUX.1-dev model by Black Forest Labs",
|
||||
"reference": "https://huggingface.co/XLabs-AI/flux-lora-collection",
|
||||
"filename": "mjv6_lora.safetensors",
|
||||
"url": "https://huggingface.co/XLabs-AI/flux-lora-collection/resolve/main/mjv6_lora.safetensors",
|
||||
"size": "44.8MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "XLabs-AI/flux-ip-adapter",
|
||||
"type": "lora",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "xlabs/ipadapters",
|
||||
"description": "A checkpoint with trained LoRAs for FLUX.1-dev model by Black Forest Labs",
|
||||
"reference": "https://huggingface.co/XLabs-AI/flux-ip-adapter",
|
||||
"filename": "ip_adapter.safetensors",
|
||||
"url": "https://huggingface.co/XLabs-AI/flux-ip-adapter/resolve/main/ip_adapter.safetensors",
|
||||
"size": "982MB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "stabilityai/SD3.5-Large-Controlnet-Blur",
|
||||
"type": "controlnet",
|
||||
"base": "SD3.5",
|
||||
"save_path": "controlnet/SD3.5",
|
||||
"description": "Blur Controlnet model for SD3.5 Large",
|
||||
"reference": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets",
|
||||
"filename": "sd3.5_large_controlnet_blur.safetensors",
|
||||
"url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets/resolve/main/sd3.5_large_controlnet_blur.safetensors",
|
||||
"size": "8.65GB"
|
||||
},
|
||||
{
|
||||
"name": "stabilityai/SD3.5-Large-Controlnet-Canny",
|
||||
"type": "controlnet",
|
||||
"base": "SD3.5",
|
||||
"save_path": "controlnet/SD3.5",
|
||||
"description": "Canny Controlnet model for SD3.5 Large",
|
||||
"reference": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets",
|
||||
"filename": "sd3.5_large_controlnet_canny.safetensors",
|
||||
"url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets/resolve/main/sd3.5_large_controlnet_canny.safetensors",
|
||||
"size": "8.65GB"
|
||||
},
|
||||
{
|
||||
"name": "stabilityai/SD3.5-Large-Controlnet-Depth",
|
||||
"type": "controlnet",
|
||||
"base": "SD3.5",
|
||||
"save_path": "controlnet/SD3.5",
|
||||
"description": "Depth Controlnet model for SD3.5 Large",
|
||||
"reference": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets",
|
||||
"filename": "sd3.5_large_controlnet_depth.safetensors",
|
||||
"url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets/resolve/main/sd3.5_large_controlnet_depth.safetensors",
|
||||
"size": "8.65GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "LTX-Video 2B v0.9 Checkpoint",
|
||||
"type": "checkpoint",
|
||||
"base": "LTX-Video",
|
||||
"save_path": "checkpoints/LTXV",
|
||||
"description": "LTX-Video is the first DiT-based video generation model capable of generating high-quality videos in real-time. It produces 24 FPS videos at a 768x512 resolution faster than they can be watched. Trained on a large-scale dataset of diverse videos, the model generates high-resolution videos with realistic and varied content.",
|
||||
"reference": "https://huggingface.co/Lightricks/LTX-Video",
|
||||
"filename": "ltx-video-2b-v0.9.safetensors",
|
||||
"url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.safetensors",
|
||||
"size": "9.37GB"
|
||||
},
|
||||
{
|
||||
"name": "InstantX/FLUX.1-dev-IP-Adapter",
|
||||
"type": "IP-Adapter",
|
||||
"base": "FLUX.1",
|
||||
"save_path": "ipadapter-flux",
|
||||
"description": "FLUX.1-dev-IP-Adapter",
|
||||
"reference": "https://huggingface.co/InstantX/FLUX.1-dev-IP-Adapter",
|
||||
"filename": "ip-adapter.bin",
|
||||
"url": "https://huggingface.co/InstantX/FLUX.1-dev-IP-Adapter/resolve/main/ip-adapter.bin",
|
||||
"size": "5.29GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Comfy-Org/sigclip_vision_384 (patch14_384)",
|
||||
"type": "clip_vision",
|
||||
"base": "sigclip",
|
||||
"save_path": "clip_vision",
|
||||
"description": "This clip vision model is required for FLUX.1 Redux.",
|
||||
"reference": "https://huggingface.co/Comfy-Org/sigclip_vision_384/tree/main",
|
||||
"filename": "sigclip_vision_patch14_384.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors",
|
||||
"size": "857MB"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,16 @@
|
||||
"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": "comfyui-wiki",
|
||||
"title": "ComfyUI-i18n-demo",
|
||||
"reference": "https://github.com/comfyui-wiki/ComfyUI-i18n-demo",
|
||||
"files": [
|
||||
"https://github.com/comfyui-wiki/ComfyUI-i18n-demo"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "ComfyUI custom node develop i18n support demo "
|
||||
},
|
||||
{
|
||||
"author": "Suzie1",
|
||||
"title": "Guide To Making Custom Nodes in ComfyUI",
|
||||
@@ -331,6 +341,36 @@
|
||||
],
|
||||
"description": "Dynamic Node examples for ComfyUI",
|
||||
"install_type": "git-clone"
|
||||
},
|
||||
{
|
||||
"author": "Jonathon-Doran",
|
||||
"title": "remote-combo-demo",
|
||||
"reference": "https://github.com/Jonathon-Doran/remote-combo-demo",
|
||||
"files": [
|
||||
"https://github.com/Jonathon-Doran/remote-combo-demo"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A minimal test suite demonstrating how remote COMBO inputs behave in ComfyUI, with and without force_input"
|
||||
},
|
||||
{
|
||||
"author": "J1mB091",
|
||||
"title": "ComfyUI-J1mB091 Custom Nodes",
|
||||
"reference": "https://github.com/J1mB091/ComfyUI-J1mB091",
|
||||
"files": [
|
||||
"https://github.com/J1mB091/ComfyUI-J1mB091"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Vibe Coded ComfyUI Custom Nodes"
|
||||
},
|
||||
{
|
||||
"author": "aiforhumans",
|
||||
"title": "XDev Nodes - Complete Toolkit",
|
||||
"reference": "https://github.com/aiforhumans/comfyui-xdev-nodes",
|
||||
"files": [
|
||||
"https://github.com/aiforhumans/comfyui-xdev-nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Complete ComfyUI development toolkit with 8 professional nodes including VAE tools, universal type testing, and comprehensive debugging infrastructure."
|
||||
}
|
||||
]
|
||||
}
|
||||
+166
-13
@@ -18,6 +18,14 @@ security: []
|
||||
# Common API components
|
||||
components:
|
||||
schemas:
|
||||
OperationType:
|
||||
type: string
|
||||
enum: [install, uninstall, update, update-comfyui, fix, disable, enable, install-model]
|
||||
description: Type of operation or task being performed
|
||||
OperationResult:
|
||||
type: string
|
||||
enum: [success, failed, skipped, error, skip]
|
||||
description: Result status of an operation (failed/error and skipped/skip are aliases)
|
||||
# Core Task Queue Models
|
||||
QueueTaskItem:
|
||||
type: object
|
||||
@@ -29,9 +37,7 @@ components:
|
||||
type: string
|
||||
description: Client identifier that initiated the task
|
||||
kind:
|
||||
type: string
|
||||
description: Type of task being performed
|
||||
enum: [install, uninstall, update, update-all, update-comfyui, fix, disable, enable, install-model]
|
||||
$ref: '#/components/schemas/OperationType'
|
||||
params:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/InstallPackParams'
|
||||
@@ -65,14 +71,19 @@ components:
|
||||
description: Task result message or details
|
||||
status:
|
||||
$ref: '#/components/schemas/TaskExecutionStatus'
|
||||
batch_id:
|
||||
type: [string, 'null']
|
||||
description: ID of the batch this task belongs to
|
||||
end_time:
|
||||
type: [string, 'null']
|
||||
format: date-time
|
||||
description: ISO timestamp when task execution ended
|
||||
required: [ui_id, client_id, kind, timestamp, result]
|
||||
TaskExecutionStatus:
|
||||
type: object
|
||||
properties:
|
||||
status_str:
|
||||
type: string
|
||||
enum: [success, error, skip]
|
||||
description: Overall task execution status
|
||||
$ref: '#/components/schemas/OperationResult'
|
||||
completed:
|
||||
type: boolean
|
||||
description: Whether the task completed
|
||||
@@ -223,6 +234,14 @@ components:
|
||||
type: string
|
||||
enum: [git-clone, copy, cnr]
|
||||
description: Type of installation used for the pack
|
||||
SecurityLevel:
|
||||
type: string
|
||||
enum: [strong, normal, normal-, weak]
|
||||
description: Security level configuration (from most to least restrictive)
|
||||
RiskLevel:
|
||||
type: string
|
||||
enum: [block, high+, high, middle+, middle]
|
||||
description: Risk classification for operations
|
||||
ManagerPack:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ManagerPackInfo'
|
||||
@@ -235,7 +254,7 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Files included in the pack
|
||||
description: Repository URLs for installation (typically contains one GitHub URL)
|
||||
reference:
|
||||
type: string
|
||||
description: The type of installation reference
|
||||
@@ -366,6 +385,46 @@ components:
|
||||
type: string
|
||||
description: ComfyUI Node Registry ID of the package to enable
|
||||
required: [cnr_id]
|
||||
# Query Parameter Models
|
||||
UpdateAllQueryParams:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
description: Client identifier that initiated the request
|
||||
ui_id:
|
||||
type: string
|
||||
description: Base UI identifier for task tracking
|
||||
mode:
|
||||
$ref: '#/components/schemas/ManagerDatabaseSource'
|
||||
required: [client_id, ui_id]
|
||||
UpdateComfyUIQueryParams:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
description: Client identifier that initiated the request
|
||||
ui_id:
|
||||
type: string
|
||||
description: UI identifier for task tracking
|
||||
stable:
|
||||
type: boolean
|
||||
default: true
|
||||
description: Whether to update to stable version (true) or nightly (false)
|
||||
required: [client_id, ui_id]
|
||||
ComfyUISwitchVersionQueryParams:
|
||||
type: object
|
||||
properties:
|
||||
ver:
|
||||
type: string
|
||||
description: Version to switch to
|
||||
client_id:
|
||||
type: string
|
||||
description: Client identifier that initiated the request
|
||||
ui_id:
|
||||
type: string
|
||||
description: UI identifier for task tracking
|
||||
required: [ver, client_id, ui_id]
|
||||
# Queue Status Models
|
||||
QueueStatus:
|
||||
type: object
|
||||
@@ -580,9 +639,7 @@ components:
|
||||
type: string
|
||||
description: Unique operation identifier
|
||||
operation_type:
|
||||
type: string
|
||||
description: Type of operation
|
||||
enum: [install, update, uninstall, fix, disable, enable, install-model]
|
||||
$ref: '#/components/schemas/OperationType'
|
||||
target:
|
||||
type: string
|
||||
description: Target of the operation (node name, model name, etc.)
|
||||
@@ -590,9 +647,7 @@ components:
|
||||
type: [string, 'null']
|
||||
description: Target version for the operation
|
||||
result:
|
||||
type: string
|
||||
description: Operation result
|
||||
enum: [success, failed, skipped]
|
||||
$ref: '#/components/schemas/OperationResult'
|
||||
error_message:
|
||||
type: [string, 'null']
|
||||
description: Error message if operation failed
|
||||
@@ -640,6 +695,45 @@ components:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: ComfyUI Manager configuration settings
|
||||
comfyui_root_path:
|
||||
type: [string, 'null']
|
||||
description: ComfyUI root installation directory
|
||||
model_paths:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Map of model types to their configured paths
|
||||
manager_version:
|
||||
type: [string, 'null']
|
||||
description: ComfyUI Manager version
|
||||
security_level:
|
||||
$ref: '#/components/schemas/SecurityLevel'
|
||||
network_mode:
|
||||
type: [string, 'null']
|
||||
description: Network mode (online, offline, private)
|
||||
cli_args:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Selected ComfyUI CLI arguments
|
||||
custom_nodes_count:
|
||||
type: [integer, 'null']
|
||||
description: Total number of custom node packages
|
||||
minimum: 0
|
||||
failed_imports:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of custom nodes that failed to import
|
||||
pip_packages:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Map of installed pip packages to their versions
|
||||
embedded_python:
|
||||
type: [boolean, 'null']
|
||||
description: Whether ComfyUI is running from an embedded Python distribution
|
||||
required: [snapshot_time, comfyui_version, python_version, platform_info]
|
||||
BatchExecutionRecord:
|
||||
type: object
|
||||
@@ -688,6 +782,39 @@ components:
|
||||
minimum: 0
|
||||
default: 0
|
||||
required: [batch_id, start_time, state_before]
|
||||
|
||||
ImportFailInfoBulkRequest:
|
||||
type: object
|
||||
properties:
|
||||
cnr_ids:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: A list of CNR IDs to check.
|
||||
urls:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: A list of repository URLs to check.
|
||||
|
||||
ImportFailInfoBulkResponse:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/ImportFailInfoItem'
|
||||
description: >-
|
||||
A dictionary where each key is a cnr_id or url from the request,
|
||||
and the value is the corresponding error info.
|
||||
|
||||
ImportFailInfoItem:
|
||||
oneOf:
|
||||
- type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
traceback:
|
||||
type: string
|
||||
- type: "null"
|
||||
|
||||
securitySchemes:
|
||||
securityLevel:
|
||||
type: apiKey
|
||||
@@ -923,6 +1050,32 @@ paths:
|
||||
description: Processing started
|
||||
'201':
|
||||
description: Processing already in progress
|
||||
|
||||
/v2/customnode/import_fail_info_bulk:
|
||||
post:
|
||||
summary: Get import failure info for multiple nodes
|
||||
description: Retrieves recorded import failure information for a list of custom nodes.
|
||||
tags:
|
||||
- customnode
|
||||
requestBody:
|
||||
description: A list of CNR IDs or repository URLs to check.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ImportFailInfoBulkRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A dictionary containing the import failure information.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ImportFailInfoBulkResponse'
|
||||
'400':
|
||||
description: Bad Request. The request body is invalid.
|
||||
'500':
|
||||
description: Internal Server Error.
|
||||
|
||||
/v2/manager/queue/reset:
|
||||
get:
|
||||
summary: Reset queue
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "comfyui-manager"
|
||||
license = { text = "GPL-3.0-only" }
|
||||
version = "4.0.0-beta.4"
|
||||
version = "4.0.4"
|
||||
requires-python = ">= 3.9"
|
||||
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
|
||||
readme = "README.md"
|
||||
@@ -19,7 +19,7 @@ maintainers = [
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
]
|
||||
@@ -27,7 +27,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"GitPython",
|
||||
"PyGithub",
|
||||
"matrix-client==0.4.0",
|
||||
# "matrix-nio",
|
||||
"transformers",
|
||||
"huggingface-hub>0.20",
|
||||
"typer",
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
GitPython
|
||||
PyGithub
|
||||
matrix-client==0.4.0
|
||||
# matrix-nio
|
||||
transformers
|
||||
huggingface-hub>0.20
|
||||
huggingface-hub
|
||||
typer
|
||||
rich
|
||||
typing-extensions
|
||||
|
||||
@@ -9,4 +9,4 @@ lint.select = [
|
||||
"F",
|
||||
]
|
||||
|
||||
exclude = ["*.ipynb"]
|
||||
exclude = ["*.ipynb", "tests"]
|
||||
|
||||
+359
-66
@@ -7,13 +7,15 @@ import concurrent
|
||||
import datetime
|
||||
import concurrent.futures
|
||||
import requests
|
||||
import warnings
|
||||
import argparse
|
||||
|
||||
builtin_nodes = set()
|
||||
|
||||
import sys
|
||||
|
||||
from urllib.parse import urlparse
|
||||
from github import Github
|
||||
from github import Github, Auth
|
||||
|
||||
|
||||
def download_url(url, dest_folder, filename=None):
|
||||
@@ -39,26 +41,51 @@ def download_url(url, dest_folder, filename=None):
|
||||
raise Exception(f"Failed to download file from {url}")
|
||||
|
||||
|
||||
# prepare temp dir
|
||||
if len(sys.argv) > 1:
|
||||
temp_dir = sys.argv[1]
|
||||
else:
|
||||
temp_dir = os.path.join(os.getcwd(), ".tmp")
|
||||
def parse_arguments():
|
||||
"""Parse command-line arguments"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='ComfyUI Manager Node Scanner',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog='''
|
||||
Examples:
|
||||
# Standard mode
|
||||
python3 scanner.py
|
||||
python3 scanner.py --skip-update
|
||||
|
||||
if not os.path.exists(temp_dir):
|
||||
os.makedirs(temp_dir)
|
||||
# Scan-only mode
|
||||
python3 scanner.py --scan-only temp-urls-clean.list
|
||||
python3 scanner.py --scan-only urls.list --temp-dir /custom/temp
|
||||
python3 scanner.py --scan-only urls.list --skip-update
|
||||
'''
|
||||
)
|
||||
|
||||
parser.add_argument('--scan-only', type=str, metavar='URL_LIST_FILE',
|
||||
help='Scan-only mode: provide URL list file (one URL per line)')
|
||||
parser.add_argument('--temp-dir', type=str, metavar='DIR',
|
||||
help='Temporary directory for cloned repositories')
|
||||
parser.add_argument('--skip-update', action='store_true',
|
||||
help='Skip git clone/pull operations')
|
||||
parser.add_argument('--skip-stat-update', action='store_true',
|
||||
help='Skip GitHub stats collection')
|
||||
parser.add_argument('--skip-all', action='store_true',
|
||||
help='Skip all update operations')
|
||||
|
||||
# Backward compatibility: positional argument for temp_dir
|
||||
parser.add_argument('temp_dir_positional', nargs='?', metavar='TEMP_DIR',
|
||||
help='(Legacy) Temporary directory path')
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
skip_update = '--skip-update' in sys.argv or '--skip-all' in sys.argv
|
||||
skip_stat_update = '--skip-stat-update' in sys.argv or '--skip-all' in sys.argv
|
||||
|
||||
if not skip_stat_update:
|
||||
g = Github(os.environ.get('GITHUB_TOKEN'))
|
||||
else:
|
||||
g = None
|
||||
|
||||
|
||||
print(f"TEMP DIR: {temp_dir}")
|
||||
# Module-level variables (will be set in main if running as script)
|
||||
args = None
|
||||
scan_only_mode = False
|
||||
url_list_file = None
|
||||
temp_dir = None
|
||||
skip_update = False
|
||||
skip_stat_update = True
|
||||
g = None
|
||||
|
||||
|
||||
parse_cnt = 0
|
||||
@@ -73,12 +100,22 @@ def extract_nodes(code_text):
|
||||
parse_cnt += 1
|
||||
|
||||
code_text = re.sub(r'\\[^"\']', '', code_text)
|
||||
parsed_code = ast.parse(code_text)
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore', category=SyntaxWarning)
|
||||
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
||||
parsed_code = ast.parse(code_text)
|
||||
|
||||
# Support both ast.Assign and ast.AnnAssign (for type-annotated assignments)
|
||||
assignments = (node for node in parsed_code.body if isinstance(node, (ast.Assign, ast.AnnAssign)))
|
||||
|
||||
assignments = (node for node in parsed_code.body if isinstance(node, ast.Assign))
|
||||
|
||||
for assignment in assignments:
|
||||
if isinstance(assignment.targets[0], ast.Name) and assignment.targets[0].id in ['NODE_CONFIG', 'NODE_CLASS_MAPPINGS']:
|
||||
# Handle ast.AnnAssign (e.g., NODE_CLASS_MAPPINGS: Type = {...})
|
||||
if isinstance(assignment, ast.AnnAssign):
|
||||
if isinstance(assignment.target, ast.Name) and assignment.target.id in ['NODE_CONFIG', 'NODE_CLASS_MAPPINGS']:
|
||||
node_class_mappings = assignment.value
|
||||
break
|
||||
# Handle ast.Assign (e.g., NODE_CLASS_MAPPINGS = {...})
|
||||
elif isinstance(assignment.targets[0], ast.Name) and assignment.targets[0].id in ['NODE_CONFIG', 'NODE_CLASS_MAPPINGS']:
|
||||
node_class_mappings = assignment.value
|
||||
break
|
||||
else:
|
||||
@@ -90,7 +127,7 @@ def extract_nodes(code_text):
|
||||
for key in node_class_mappings.keys:
|
||||
if key is not None and isinstance(key.value, str):
|
||||
s.add(key.value.strip())
|
||||
|
||||
|
||||
return s
|
||||
else:
|
||||
return set()
|
||||
@@ -98,6 +135,99 @@ def extract_nodes(code_text):
|
||||
return set()
|
||||
|
||||
|
||||
def has_comfy_node_base(class_node):
|
||||
"""Check if class inherits from io.ComfyNode or ComfyNode"""
|
||||
for base in class_node.bases:
|
||||
# Case 1: ComfyNode
|
||||
if isinstance(base, ast.Name) and base.id == 'ComfyNode':
|
||||
return True
|
||||
# Case 2: io.ComfyNode
|
||||
elif isinstance(base, ast.Attribute):
|
||||
if base.attr == 'ComfyNode':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_keyword_value(call_node, keyword):
|
||||
"""
|
||||
Extract string value of keyword argument
|
||||
Schema(node_id="MyNode") -> "MyNode"
|
||||
"""
|
||||
for kw in call_node.keywords:
|
||||
if kw.arg == keyword:
|
||||
# ast.Constant (Python 3.8+)
|
||||
if isinstance(kw.value, ast.Constant):
|
||||
if isinstance(kw.value.value, str):
|
||||
return kw.value.value
|
||||
# ast.Str (Python 3.7-) - suppress deprecation warning
|
||||
else:
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
||||
if hasattr(ast, 'Str') and isinstance(kw.value, ast.Str):
|
||||
return kw.value.s
|
||||
return None
|
||||
|
||||
|
||||
def is_schema_call(call_node):
|
||||
"""Check if ast.Call is io.Schema() or Schema()"""
|
||||
func = call_node.func
|
||||
if isinstance(func, ast.Name) and func.id == 'Schema':
|
||||
return True
|
||||
elif isinstance(func, ast.Attribute) and func.attr == 'Schema':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_node_id_from_schema(class_node):
|
||||
"""
|
||||
Extract node_id from define_schema() method
|
||||
"""
|
||||
for item in class_node.body:
|
||||
if isinstance(item, ast.FunctionDef) and item.name == 'define_schema':
|
||||
# Walk through function body
|
||||
for stmt in ast.walk(item):
|
||||
if isinstance(stmt, ast.Call):
|
||||
# Check if it's Schema() call
|
||||
if is_schema_call(stmt):
|
||||
node_id = extract_keyword_value(stmt, 'node_id')
|
||||
if node_id:
|
||||
return node_id
|
||||
return None
|
||||
|
||||
|
||||
def extract_v3_nodes(code_text):
|
||||
"""
|
||||
Extract V3 node IDs using AST parsing
|
||||
Returns: set of node_id strings
|
||||
"""
|
||||
global parse_cnt
|
||||
|
||||
try:
|
||||
if parse_cnt % 100 == 0:
|
||||
print(".", end="", flush=True)
|
||||
parse_cnt += 1
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore', category=SyntaxWarning)
|
||||
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
||||
tree = ast.parse(code_text)
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
return set()
|
||||
|
||||
nodes = set()
|
||||
|
||||
# Find io.ComfyNode subclasses
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
# Check if inherits from ComfyNode
|
||||
if has_comfy_node_base(node):
|
||||
node_id = extract_node_id_from_schema(node)
|
||||
if node_id:
|
||||
nodes.add(node_id)
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
# scan
|
||||
def scan_in_file(filename, is_builtin=False):
|
||||
global builtin_nodes
|
||||
@@ -105,13 +235,18 @@ def scan_in_file(filename, is_builtin=False):
|
||||
with open(filename, encoding='utf-8', errors='ignore') as file:
|
||||
code = file.read()
|
||||
|
||||
pattern = r"_CLASS_MAPPINGS\s*=\s*{([^}]*)}"
|
||||
# Support type annotations (e.g., NODE_CLASS_MAPPINGS: Type = {...}) and line continuations (\)
|
||||
pattern = r"_CLASS_MAPPINGS\s*(?::\s*\w+\s*)?=\s*(?:\\\s*)?{([^}]*)}"
|
||||
regex = re.compile(pattern, re.MULTILINE | re.DOTALL)
|
||||
|
||||
nodes = set()
|
||||
class_dict = {}
|
||||
|
||||
# V1 nodes detection
|
||||
nodes |= extract_nodes(code)
|
||||
|
||||
# V3 nodes detection
|
||||
nodes |= extract_v3_nodes(code)
|
||||
code = re.sub(r'^#.*?$', '', code, flags=re.MULTILINE)
|
||||
|
||||
def extract_keys(pattern, code):
|
||||
@@ -208,6 +343,53 @@ def get_nodes(target_dir):
|
||||
return py_files, directories
|
||||
|
||||
|
||||
def get_urls_from_list_file(list_file):
|
||||
"""
|
||||
Read URLs from list file for scan-only mode
|
||||
|
||||
Args:
|
||||
list_file (str): Path to URL list file (one URL per line)
|
||||
|
||||
Returns:
|
||||
list of tuples: [(url, "", None, None), ...]
|
||||
Format: (url, title, preemptions, nodename_pattern)
|
||||
- title: Empty string
|
||||
- preemptions: None
|
||||
- nodename_pattern: None
|
||||
|
||||
File format:
|
||||
https://github.com/owner/repo1
|
||||
https://github.com/owner/repo2
|
||||
# Comments starting with # are ignored
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If list_file does not exist
|
||||
"""
|
||||
if not os.path.exists(list_file):
|
||||
raise FileNotFoundError(f"URL list file not found: {list_file}")
|
||||
|
||||
urls = []
|
||||
with open(list_file, 'r', encoding='utf-8') as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
line = line.strip()
|
||||
|
||||
# Skip empty lines and comments
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
# Validate URL format (basic check)
|
||||
if not (line.startswith('http://') or line.startswith('https://')):
|
||||
print(f"WARNING: Line {line_num} is not a valid URL: {line}")
|
||||
continue
|
||||
|
||||
# Add URL with empty metadata
|
||||
# (url, title, preemptions, nodename_pattern)
|
||||
urls.append((line, "", None, None))
|
||||
|
||||
print(f"Loaded {len(urls)} URLs from {list_file}")
|
||||
return urls
|
||||
|
||||
|
||||
def get_git_urls_from_json(json_file):
|
||||
with open(json_file, encoding='utf-8') as file:
|
||||
data = json.load(file)
|
||||
@@ -255,22 +437,52 @@ def clone_or_pull_git_repository(git_url):
|
||||
repo.git.submodule('update', '--init', '--recursive')
|
||||
print(f"Pulling {repo_name}...")
|
||||
except Exception as e:
|
||||
print(f"Pulling {repo_name} failed: {e}")
|
||||
print(f"Failed to pull '{repo_name}': {e}")
|
||||
else:
|
||||
try:
|
||||
Repo.clone_from(git_url, repo_dir, recursive=True)
|
||||
print(f"Cloning {repo_name}...")
|
||||
except Exception as e:
|
||||
print(f"Cloning {repo_name} failed: {e}")
|
||||
print(f"Failed to clone '{repo_name}': {e}")
|
||||
|
||||
|
||||
def update_custom_nodes():
|
||||
def update_custom_nodes(scan_only_mode=False, url_list_file=None):
|
||||
"""
|
||||
Update custom nodes by cloning/pulling repositories
|
||||
|
||||
Args:
|
||||
scan_only_mode (bool): If True, use URL list file instead of custom-node-list.json
|
||||
url_list_file (str): Path to URL list file (required if scan_only_mode=True)
|
||||
|
||||
Returns:
|
||||
dict: node_info mapping {repo_name: (url, title, preemptions, node_pattern)}
|
||||
"""
|
||||
if not os.path.exists(temp_dir):
|
||||
os.makedirs(temp_dir)
|
||||
|
||||
node_info = {}
|
||||
|
||||
git_url_titles_preemptions = get_git_urls_from_json('custom-node-list.json')
|
||||
# Select URL source based on mode
|
||||
if scan_only_mode:
|
||||
if not url_list_file:
|
||||
raise ValueError("url_list_file is required in scan-only mode")
|
||||
|
||||
git_url_titles_preemptions = get_urls_from_list_file(url_list_file)
|
||||
print("\n[Scan-Only Mode]")
|
||||
print(f" - URL source: {url_list_file}")
|
||||
print(" - GitHub stats: DISABLED")
|
||||
print(f" - Git clone/pull: {'ENABLED' if not skip_update else 'DISABLED'}")
|
||||
print(" - Metadata: EMPTY")
|
||||
else:
|
||||
if not os.path.exists('custom-node-list.json'):
|
||||
raise FileNotFoundError("custom-node-list.json not found")
|
||||
|
||||
git_url_titles_preemptions = get_git_urls_from_json('custom-node-list.json')
|
||||
print("\n[Standard Mode]")
|
||||
print(" - URL source: custom-node-list.json")
|
||||
print(f" - GitHub stats: {'ENABLED' if not skip_stat_update else 'DISABLED'}")
|
||||
print(f" - Git clone/pull: {'ENABLED' if not skip_update else 'DISABLED'}")
|
||||
print(" - Metadata: FULL")
|
||||
|
||||
def process_git_url_title(url, title, preemptions, node_pattern):
|
||||
name = os.path.basename(url)
|
||||
@@ -382,46 +594,59 @@ def update_custom_nodes():
|
||||
if not skip_stat_update:
|
||||
process_git_stats(git_url_titles_preemptions)
|
||||
|
||||
# Git clone/pull for all repositories
|
||||
with concurrent.futures.ThreadPoolExecutor(11) as executor:
|
||||
for url, title, preemptions, node_pattern in git_url_titles_preemptions:
|
||||
executor.submit(process_git_url_title, url, title, preemptions, node_pattern)
|
||||
|
||||
py_url_titles_and_pattern = get_py_urls_from_json('custom-node-list.json')
|
||||
# .py file download (skip in scan-only mode - only process git repos)
|
||||
if not scan_only_mode:
|
||||
py_url_titles_and_pattern = get_py_urls_from_json('custom-node-list.json')
|
||||
|
||||
def download_and_store_info(url_title_preemptions_and_pattern):
|
||||
url, title, preemptions, node_pattern = url_title_preemptions_and_pattern
|
||||
name = os.path.basename(url)
|
||||
if name.endswith(".py"):
|
||||
node_info[name] = (url, title, preemptions, node_pattern)
|
||||
def download_and_store_info(url_title_preemptions_and_pattern):
|
||||
url, title, preemptions, node_pattern = url_title_preemptions_and_pattern
|
||||
name = os.path.basename(url)
|
||||
if name.endswith(".py"):
|
||||
node_info[name] = (url, title, preemptions, node_pattern)
|
||||
|
||||
try:
|
||||
download_url(url, temp_dir)
|
||||
except Exception:
|
||||
print(f"[ERROR] Cannot download '{url}'")
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(10) as executor:
|
||||
executor.map(download_and_store_info, py_url_titles_and_pattern)
|
||||
with concurrent.futures.ThreadPoolExecutor(10) as executor:
|
||||
executor.map(download_and_store_info, py_url_titles_and_pattern)
|
||||
|
||||
return node_info
|
||||
|
||||
|
||||
def gen_json(node_info):
|
||||
def gen_json(node_info, scan_only_mode=False):
|
||||
"""
|
||||
Generate extension-node-map.json from scanned node information
|
||||
|
||||
Args:
|
||||
node_info (dict): Repository metadata mapping
|
||||
scan_only_mode (bool): If True, exclude metadata from output
|
||||
"""
|
||||
# scan from .py file
|
||||
node_files, node_dirs = get_nodes(temp_dir)
|
||||
|
||||
comfyui_path = os.path.abspath(os.path.join(temp_dir, "ComfyUI"))
|
||||
node_dirs.remove(comfyui_path)
|
||||
node_dirs = [comfyui_path] + node_dirs
|
||||
# Only reorder if ComfyUI exists in the list
|
||||
if comfyui_path in node_dirs:
|
||||
node_dirs.remove(comfyui_path)
|
||||
node_dirs = [comfyui_path] + node_dirs
|
||||
|
||||
data = {}
|
||||
for dirname in node_dirs:
|
||||
py_files = get_py_file_paths(dirname)
|
||||
metadata = {}
|
||||
|
||||
|
||||
nodes = set()
|
||||
for py in py_files:
|
||||
nodes_in_file, metadata_in_file = scan_in_file(py, dirname == "ComfyUI")
|
||||
nodes.update(nodes_in_file)
|
||||
# Include metadata from .py files in both modes
|
||||
metadata.update(metadata_in_file)
|
||||
|
||||
dirname = os.path.basename(dirname)
|
||||
@@ -436,17 +661,28 @@ def gen_json(node_info):
|
||||
if dirname in node_info:
|
||||
git_url, title, preemptions, node_pattern = node_info[dirname]
|
||||
|
||||
metadata['title_aux'] = title
|
||||
# Conditionally add metadata based on mode
|
||||
if not scan_only_mode:
|
||||
# Standard mode: include all metadata
|
||||
metadata['title_aux'] = title
|
||||
|
||||
if preemptions is not None:
|
||||
metadata['preemptions'] = preemptions
|
||||
if preemptions is not None:
|
||||
metadata['preemptions'] = preemptions
|
||||
|
||||
if node_pattern is not None:
|
||||
metadata['nodename_pattern'] = node_pattern
|
||||
if node_pattern is not None:
|
||||
metadata['nodename_pattern'] = node_pattern
|
||||
# Scan-only mode: metadata remains empty
|
||||
|
||||
data[git_url] = (nodes, metadata)
|
||||
else:
|
||||
print(f"WARN: {dirname} is removed from custom-node-list.json")
|
||||
# Scan-only mode: Repository not in node_info (expected behavior)
|
||||
# Construct URL from dirname (author_repo format)
|
||||
if '_' in dirname:
|
||||
parts = dirname.split('_', 1)
|
||||
git_url = f"https://github.com/{parts[0]}/{parts[1]}"
|
||||
data[git_url] = (nodes, metadata)
|
||||
else:
|
||||
print(f"WARN: {dirname} is removed from custom-node-list.json")
|
||||
|
||||
for file in node_files:
|
||||
nodes, metadata = scan_in_file(file)
|
||||
@@ -459,13 +695,16 @@ def gen_json(node_info):
|
||||
|
||||
if file in node_info:
|
||||
url, title, preemptions, node_pattern = node_info[file]
|
||||
metadata['title_aux'] = title
|
||||
|
||||
if preemptions is not None:
|
||||
metadata['preemptions'] = preemptions
|
||||
|
||||
if node_pattern is not None:
|
||||
metadata['nodename_pattern'] = node_pattern
|
||||
# Conditionally add metadata based on mode
|
||||
if not scan_only_mode:
|
||||
metadata['title_aux'] = title
|
||||
|
||||
if preemptions is not None:
|
||||
metadata['preemptions'] = preemptions
|
||||
|
||||
if node_pattern is not None:
|
||||
metadata['nodename_pattern'] = node_pattern
|
||||
|
||||
data[url] = (nodes, metadata)
|
||||
else:
|
||||
@@ -477,6 +716,10 @@ def gen_json(node_info):
|
||||
for extension in extensions:
|
||||
node_list_json_path = os.path.join(temp_dir, extension, 'node_list.json')
|
||||
if os.path.exists(node_list_json_path):
|
||||
# Skip if extension not in node_info (scan-only mode with limited URLs)
|
||||
if extension not in node_info:
|
||||
continue
|
||||
|
||||
git_url, title, preemptions, node_pattern = node_info[extension]
|
||||
|
||||
with open(node_list_json_path, 'r', encoding='utf-8') as f:
|
||||
@@ -496,17 +739,26 @@ def gen_json(node_info):
|
||||
nodes_in_url, metadata_in_url = data[git_url]
|
||||
nodes = set(nodes_in_url)
|
||||
|
||||
for x, desc in node_list_json.items():
|
||||
nodes.add(x.strip())
|
||||
try:
|
||||
for x, desc in node_list_json.items():
|
||||
nodes.add(x.strip())
|
||||
except Exception as e:
|
||||
print(f"\nERROR: Invalid json format '{node_list_json_path}'")
|
||||
print("------------------------------------------------------")
|
||||
print(e)
|
||||
print("------------------------------------------------------")
|
||||
node_list_json = {}
|
||||
|
||||
metadata_in_url['title_aux'] = title
|
||||
# Conditionally add metadata based on mode
|
||||
if not scan_only_mode:
|
||||
metadata_in_url['title_aux'] = title
|
||||
|
||||
if preemptions is not None:
|
||||
metadata['preemptions'] = preemptions
|
||||
if preemptions is not None:
|
||||
metadata_in_url['preemptions'] = preemptions
|
||||
|
||||
if node_pattern is not None:
|
||||
metadata_in_url['nodename_pattern'] = node_pattern
|
||||
|
||||
if node_pattern is not None:
|
||||
metadata_in_url['nodename_pattern'] = node_pattern
|
||||
|
||||
nodes = list(nodes)
|
||||
nodes.sort()
|
||||
data[git_url] = (nodes, metadata_in_url)
|
||||
@@ -516,12 +768,53 @@ def gen_json(node_info):
|
||||
json.dump(data, file, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
print("### ComfyUI Manager Node Scanner ###")
|
||||
if __name__ == "__main__":
|
||||
# Parse arguments
|
||||
args = parse_arguments()
|
||||
|
||||
print("\n# Updating extensions\n")
|
||||
updated_node_info = update_custom_nodes()
|
||||
# Determine mode
|
||||
scan_only_mode = args.scan_only is not None
|
||||
url_list_file = args.scan_only if scan_only_mode else None
|
||||
|
||||
print("\n# 'extension-node-map.json' file is generated.\n")
|
||||
gen_json(updated_node_info)
|
||||
# Determine temp_dir
|
||||
if args.temp_dir:
|
||||
temp_dir = args.temp_dir
|
||||
elif args.temp_dir_positional:
|
||||
temp_dir = args.temp_dir_positional
|
||||
else:
|
||||
temp_dir = os.path.join(os.getcwd(), ".tmp")
|
||||
|
||||
print("\nDONE.\n")
|
||||
if not os.path.exists(temp_dir):
|
||||
os.makedirs(temp_dir)
|
||||
|
||||
# Determine skip flags
|
||||
skip_update = args.skip_update or args.skip_all
|
||||
skip_stat_update = args.skip_stat_update or args.skip_all or scan_only_mode
|
||||
|
||||
if not skip_stat_update:
|
||||
auth = Auth.Token(os.environ.get('GITHUB_TOKEN'))
|
||||
g = Github(auth=auth)
|
||||
else:
|
||||
g = None
|
||||
|
||||
print("### ComfyUI Manager Node Scanner ###")
|
||||
|
||||
if scan_only_mode:
|
||||
print(f"\n# [Scan-Only Mode] Processing URL list: {url_list_file}\n")
|
||||
else:
|
||||
print("\n# [Standard Mode] Updating extensions\n")
|
||||
|
||||
# Update/clone repositories and collect node info
|
||||
updated_node_info = update_custom_nodes(scan_only_mode, url_list_file)
|
||||
|
||||
print("\n# Generating 'extension-node-map.json'...\n")
|
||||
|
||||
# Generate extension-node-map.json
|
||||
gen_json(updated_node_info, scan_only_mode)
|
||||
|
||||
print("\n✅ DONE.\n")
|
||||
|
||||
if scan_only_mode:
|
||||
print("Output: extension-node-map.json (node mappings only)")
|
||||
else:
|
||||
print("Output: extension-node-map.json (full metadata)")
|
||||
Reference in New Issue
Block a user